@khanacademy/math-input 0.4.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/{build/math-input.css → dist/es/index.css} +0 -150
  4. package/dist/es/index.js +7798 -0
  5. package/dist/es/index.js.map +1 -0
  6. package/dist/index.css +586 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7768 -0
  9. package/dist/index.js.flow +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/strings.js +71 -0
  12. package/index.html +20 -0
  13. package/less/echo.less +56 -0
  14. package/less/main.less +5 -0
  15. package/less/overrides.less +129 -0
  16. package/less/popover.less +22 -0
  17. package/less/tabbar.less +6 -0
  18. package/package.json +60 -89
  19. package/src/actions/index.js +57 -0
  20. package/src/components/__tests__/gesture-state-machine_test.js +437 -0
  21. package/src/components/__tests__/node-manager_test.js +89 -0
  22. package/src/components/__tests__/two-page-keypad_test.js +42 -0
  23. package/src/components/app.js +73 -0
  24. package/src/components/common-style.js +47 -0
  25. package/src/components/compute-layout-parameters.js +157 -0
  26. package/src/components/corner-decal.js +56 -0
  27. package/src/components/echo-manager.js +160 -0
  28. package/src/components/empty-keypad-button.js +49 -0
  29. package/src/components/expression-keypad.js +323 -0
  30. package/src/components/fraction-keypad.js +176 -0
  31. package/src/components/gesture-manager.js +226 -0
  32. package/src/components/gesture-state-machine.js +283 -0
  33. package/src/components/icon.js +74 -0
  34. package/src/components/iconography/arrow.js +22 -0
  35. package/src/components/iconography/backspace.js +29 -0
  36. package/src/components/iconography/cdot.js +29 -0
  37. package/src/components/iconography/cos.js +30 -0
  38. package/src/components/iconography/cube-root.js +36 -0
  39. package/src/components/iconography/dismiss.js +25 -0
  40. package/src/components/iconography/divide.js +34 -0
  41. package/src/components/iconography/down.js +16 -0
  42. package/src/components/iconography/equal.js +33 -0
  43. package/src/components/iconography/exp-2.js +29 -0
  44. package/src/components/iconography/exp-3.js +29 -0
  45. package/src/components/iconography/exp.js +29 -0
  46. package/src/components/iconography/frac.js +44 -0
  47. package/src/components/iconography/geq.js +33 -0
  48. package/src/components/iconography/gt.js +33 -0
  49. package/src/components/iconography/index.js +45 -0
  50. package/src/components/iconography/jump-into-numerator.js +41 -0
  51. package/src/components/iconography/jump-out-base.js +30 -0
  52. package/src/components/iconography/jump-out-denominator.js +41 -0
  53. package/src/components/iconography/jump-out-exponent.js +30 -0
  54. package/src/components/iconography/jump-out-numerator.js +41 -0
  55. package/src/components/iconography/jump-out-parentheses.js +33 -0
  56. package/src/components/iconography/left-paren.js +33 -0
  57. package/src/components/iconography/left.js +16 -0
  58. package/src/components/iconography/leq.js +33 -0
  59. package/src/components/iconography/ln.js +29 -0
  60. package/src/components/iconography/log-n.js +29 -0
  61. package/src/components/iconography/log.js +29 -0
  62. package/src/components/iconography/lt.js +33 -0
  63. package/src/components/iconography/minus.js +32 -0
  64. package/src/components/iconography/neq.js +33 -0
  65. package/src/components/iconography/parens.js +33 -0
  66. package/src/components/iconography/percent.js +49 -0
  67. package/src/components/iconography/period.js +26 -0
  68. package/src/components/iconography/plus.js +32 -0
  69. package/src/components/iconography/radical.js +36 -0
  70. package/src/components/iconography/right-paren.js +33 -0
  71. package/src/components/iconography/right.js +16 -0
  72. package/src/components/iconography/sin.js +30 -0
  73. package/src/components/iconography/sqrt.js +32 -0
  74. package/src/components/iconography/tan.js +30 -0
  75. package/src/components/iconography/times.js +33 -0
  76. package/src/components/iconography/up.js +16 -0
  77. package/src/components/input/__tests__/context-tracking_test.js +177 -0
  78. package/src/components/input/__tests__/math-wrapper.jsx +33 -0
  79. package/src/components/input/__tests__/mathquill_test.js +747 -0
  80. package/src/components/input/cursor-contexts.js +29 -0
  81. package/src/components/input/cursor-handle.js +137 -0
  82. package/src/components/input/drag-listener.js +75 -0
  83. package/src/components/input/math-input.js +924 -0
  84. package/src/components/input/math-wrapper.js +959 -0
  85. package/src/components/input/scroll-into-view.js +72 -0
  86. package/src/components/keypad/button-assets.js +492 -0
  87. package/src/components/keypad/button.js +106 -0
  88. package/src/components/keypad/button.stories.js +29 -0
  89. package/src/components/keypad/index.js +64 -0
  90. package/src/components/keypad/keypad-page-items.js +106 -0
  91. package/src/components/keypad/keypad-pages.stories.js +32 -0
  92. package/src/components/keypad/keypad.stories.js +35 -0
  93. package/src/components/keypad/numeric-input-page.js +100 -0
  94. package/src/components/keypad/pre-algebra-page.js +98 -0
  95. package/src/components/keypad/trigonometry-page.js +90 -0
  96. package/src/components/keypad-button.js +366 -0
  97. package/src/components/keypad-container.js +303 -0
  98. package/src/components/keypad.js +154 -0
  99. package/src/components/many-keypad-button.js +44 -0
  100. package/src/components/math-icon.js +65 -0
  101. package/src/components/multi-symbol-grid.js +182 -0
  102. package/src/components/multi-symbol-popover.js +59 -0
  103. package/src/components/navigation-pad.js +139 -0
  104. package/src/components/node-manager.js +129 -0
  105. package/src/components/popover-manager.js +76 -0
  106. package/src/components/popover-state-machine.js +173 -0
  107. package/src/components/prop-types.js +82 -0
  108. package/src/components/provided-keypad.js +103 -0
  109. package/src/components/styles.js +38 -0
  110. package/src/components/svg-icon.js +25 -0
  111. package/src/components/tabbar/__tests__/tabbar_test.js +65 -0
  112. package/src/components/tabbar/icons.js +69 -0
  113. package/src/components/tabbar/item.js +138 -0
  114. package/src/components/tabbar/tabbar.js +61 -0
  115. package/src/components/tabbar/tabbar.stories.js +60 -0
  116. package/src/components/tabbar/types.js +3 -0
  117. package/src/components/text-icon.js +52 -0
  118. package/src/components/touchable-keypad-button.js +146 -0
  119. package/src/components/two-page-keypad.js +99 -0
  120. package/src/components/velocity-tracker.js +76 -0
  121. package/src/components/z-indexes.js +9 -0
  122. package/src/consts.js +74 -0
  123. package/src/data/key-configs.js +349 -0
  124. package/src/data/keys.js +72 -0
  125. package/src/demo.js +8 -0
  126. package/src/fake-react-native-web/index.js +12 -0
  127. package/src/fake-react-native-web/text.js +56 -0
  128. package/src/fake-react-native-web/view.js +91 -0
  129. package/src/index.js +14 -0
  130. package/src/native-app.js +84 -0
  131. package/src/store/index.js +505 -0
  132. package/src/utils.js +18 -0
  133. package/tools/svg-to-react/convert.py +111 -0
  134. package/tools/svg-to-react/icons/math-keypad-icon-0.svg +32 -0
  135. package/tools/svg-to-react/icons/math-keypad-icon-1.svg +32 -0
  136. package/tools/svg-to-react/icons/math-keypad-icon-2.svg +32 -0
  137. package/tools/svg-to-react/icons/math-keypad-icon-3.svg +32 -0
  138. package/tools/svg-to-react/icons/math-keypad-icon-4.svg +32 -0
  139. package/tools/svg-to-react/icons/math-keypad-icon-5.svg +32 -0
  140. package/tools/svg-to-react/icons/math-keypad-icon-6.svg +32 -0
  141. package/tools/svg-to-react/icons/math-keypad-icon-7.svg +32 -0
  142. package/tools/svg-to-react/icons/math-keypad-icon-8.svg +32 -0
  143. package/tools/svg-to-react/icons/math-keypad-icon-9.svg +32 -0
  144. package/tools/svg-to-react/icons/math-keypad-icon-addition.svg +34 -0
  145. package/tools/svg-to-react/icons/math-keypad-icon-cos.svg +38 -0
  146. package/tools/svg-to-react/icons/math-keypad-icon-delete.svg +36 -0
  147. package/tools/svg-to-react/icons/math-keypad-icon-dismiss.svg +36 -0
  148. package/tools/svg-to-react/icons/math-keypad-icon-division.svg +36 -0
  149. package/tools/svg-to-react/icons/math-keypad-icon-equals-not.svg +50 -0
  150. package/tools/svg-to-react/icons/math-keypad-icon-equals.svg +48 -0
  151. package/tools/svg-to-react/icons/math-keypad-icon-exponent-2.svg +38 -0
  152. package/tools/svg-to-react/icons/math-keypad-icon-exponent-3.svg +38 -0
  153. package/tools/svg-to-react/icons/math-keypad-icon-exponent.svg +38 -0
  154. package/tools/svg-to-react/icons/math-keypad-icon-fraction.svg +42 -0
  155. package/tools/svg-to-react/icons/math-keypad-icon-greater-than.svg +46 -0
  156. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-base.svg +44 -0
  157. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-denominator.svg +48 -0
  158. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-exponent.svg +44 -0
  159. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-parentheses.svg +44 -0
  160. package/tools/svg-to-react/icons/math-keypad-icon-less-than.svg +46 -0
  161. package/tools/svg-to-react/icons/math-keypad-icon-log-10.svg +36 -0
  162. package/tools/svg-to-react/icons/math-keypad-icon-log-e.svg +36 -0
  163. package/tools/svg-to-react/icons/math-keypad-icon-log.svg +38 -0
  164. package/tools/svg-to-react/icons/math-keypad-icon-multiplication-cross.svg +40 -0
  165. package/tools/svg-to-react/icons/math-keypad-icon-multiplication-dot.svg +38 -0
  166. package/tools/svg-to-react/icons/math-keypad-icon-percent.svg +42 -0
  167. package/tools/svg-to-react/icons/math-keypad-icon-radical-2.svg +36 -0
  168. package/tools/svg-to-react/icons/math-keypad-icon-radical-3.svg +38 -0
  169. package/tools/svg-to-react/icons/math-keypad-icon-radical.svg +38 -0
  170. package/tools/svg-to-react/icons/math-keypad-icon-radix-character.svg +32 -0
  171. package/tools/svg-to-react/icons/math-keypad-icon-sin.svg +38 -0
  172. package/tools/svg-to-react/icons/math-keypad-icon-subtraction.svg +32 -0
  173. package/tools/svg-to-react/icons/math-keypad-icon-tan.svg +38 -0
  174. package/tools/svg-to-react/symbol_map.py +41 -0
  175. package/LICENSE.txt +0 -21
  176. package/build/math-input.js +0 -1
@@ -0,0 +1,129 @@
1
+ /**
2
+ * A manager for our node-to-ID system. In particular, this class is
3
+ * responsible for maintaing a mapping between DOM nodes and node IDs, and
4
+ * translating touch events from the raw positions at which they occur to the
5
+ * nodes over which they are occurring. This differs from browser behavior, in
6
+ * which touch events are only sent to the node in which a touch started.
7
+ */
8
+
9
+ class NodeManager {
10
+ constructor() {
11
+ // A mapping from IDs to DOM nodes.
12
+ this._nodesById = {};
13
+
14
+ // A mapping from IDs to the borders around the DOM nodes, which can be
15
+ // useful for layout purposes.
16
+ this._bordersById = {};
17
+
18
+ // An ordered list of IDs, where DOM nodes that are "higher" on the
19
+ // page come earlier in the list. Note that an ID may be present in
20
+ // this ordered list but not be registered to a DOM node (i.e., if it
21
+ // is registered as a child of another DOM node, but hasn't appeared in
22
+ // the DOM yet).
23
+ this._orderedIds = [];
24
+
25
+ // Cache bounding boxes aggressively, re-computing on page resize. Our
26
+ // caching here makes the strict assumption that if a node is reasonably
27
+ // assumed to be on-screen, its bounds won't change. For example, if we
28
+ // see that a touch occurred within the bounds of a node, we cache those
29
+ // bounds.
30
+ // TODO(charlie): It'd be great if we could pre-compute these when the
31
+ // page is idle and the keypad is visible (i.e., the nodes are in their
32
+ // proper positions).
33
+ this._cachedBoundingBoxesById = {};
34
+ window.addEventListener("resize", () => {
35
+ this._cachedBoundingBoxesById = {};
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Register a DOM node with a given identifier.
41
+ *
42
+ * @param {string} id - the identifier of the given node
43
+ * @param {node} domNode - the DOM node linked to the identifier
44
+ * @param {object} borders - an opaque object describing the node's borders
45
+ */
46
+ registerDOMNode(id, domNode, childIds, borders) {
47
+ this._nodesById[id] = domNode;
48
+ this._bordersById[id] = borders;
49
+
50
+ // Make sure that any children appear first.
51
+ // TODO(charlie): This is a very simplistic system that wouldn't
52
+ // properly handle multiple levels of nesting.
53
+ const allIds = [...(childIds || []), id, ...this._orderedIds];
54
+
55
+ // De-dupe the list of IDs.
56
+ const orderedIds = [];
57
+ const seenIds = {};
58
+ for (const id of allIds) {
59
+ if (!seenIds[id]) {
60
+ orderedIds.push(id);
61
+ seenIds[id] = true;
62
+ }
63
+ }
64
+
65
+ this._orderedIds = orderedIds;
66
+ }
67
+
68
+ /**
69
+ * Unregister the DOM node with the given identifier.
70
+ *
71
+ * @param {string} id - the identifier of the node to unregister
72
+ */
73
+ unregisterDOMNode(id) {
74
+ delete this._nodesById[id];
75
+ }
76
+
77
+ /**
78
+ * Return the identifier of the topmost node located at the given
79
+ * coordinates.
80
+ *
81
+ * @param {number} x - the x coordinate at which to search for a node
82
+ * @param {number} y - the y coordinate at which to search for a node
83
+ * @returns {null|string} - null or the identifier of the topmost node at
84
+ * the given coordinates
85
+ */
86
+ idForCoords(x, y) {
87
+ for (const id of this._orderedIds) {
88
+ const domNode = this._nodesById[id];
89
+ if (domNode) {
90
+ const bounds = domNode.getBoundingClientRect();
91
+ if (
92
+ bounds.left <= x &&
93
+ bounds.right > x &&
94
+ bounds.top <= y &&
95
+ bounds.bottom > y
96
+ ) {
97
+ this._cachedBoundingBoxesById[id] = bounds;
98
+ return id;
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Return the necessary layout information, including the bounds and border
106
+ * values, for the node with the given identifier.
107
+ *
108
+ * @param {string} id - the identifier of the node for which to return the
109
+ * layout information
110
+ * @returns {object} - the bounding client rect for the given node, along
111
+ * with its borders
112
+ */
113
+ layoutPropsForId(id) {
114
+ if (!this._cachedBoundingBoxesById[id]) {
115
+ const node = this._nodesById[id];
116
+
117
+ this._cachedBoundingBoxesById[id] = node
118
+ ? node.getBoundingClientRect()
119
+ : new DOMRect();
120
+ }
121
+
122
+ return {
123
+ initialBounds: this._cachedBoundingBoxesById[id],
124
+ borders: this._bordersById[id],
125
+ };
126
+ }
127
+ }
128
+
129
+ export default NodeManager;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A component that renders and animates the popovers that appear over the
3
+ * multi-functional keys.
4
+ */
5
+
6
+ import PropTypes from "prop-types";
7
+ import * as React from "react";
8
+ import {CSSTransition} from "react-transition-group";
9
+
10
+ import KeyConfigs from "../data/key-configs.js";
11
+
12
+ import MultiSymbolPopover from "./multi-symbol-popover.js";
13
+ import {
14
+ boundingBoxPropType,
15
+ keyConfigPropType,
16
+ popoverPropType,
17
+ } from "./prop-types.js";
18
+
19
+ // NOTE(charlie): These must be kept in sync with the transition durations and
20
+ // classnames specified in popover.less.
21
+ const animationTransitionName = "popover";
22
+ const animationDurationMs = 200;
23
+
24
+ // A container component used to position a popover absolutely at a specific
25
+ // position.
26
+ class PopoverContainer extends React.Component {
27
+ static propTypes = {
28
+ bounds: boundingBoxPropType.isRequired,
29
+ childKeys: PropTypes.arrayOf(keyConfigPropType).isRequired,
30
+ };
31
+
32
+ render() {
33
+ const {bounds, childKeys} = this.props;
34
+
35
+ const containerStyle = {
36
+ position: "absolute",
37
+ ...bounds,
38
+ };
39
+
40
+ return (
41
+ <div style={containerStyle}>
42
+ <MultiSymbolPopover keys={childKeys} />
43
+ </div>
44
+ );
45
+ }
46
+ }
47
+
48
+ class PopoverManager extends React.Component {
49
+ static propTypes = {
50
+ popover: popoverPropType,
51
+ };
52
+
53
+ render() {
54
+ const {popover} = this.props;
55
+
56
+ return popover ? (
57
+ <CSSTransition
58
+ in={true}
59
+ classNames={animationTransitionName}
60
+ enter={true}
61
+ exit={false}
62
+ timeout={{
63
+ enter: animationDurationMs,
64
+ }}
65
+ >
66
+ <PopoverContainer
67
+ key={popover.childKeyIds[0]}
68
+ bounds={popover.bounds}
69
+ childKeys={popover.childKeyIds.map((id) => KeyConfigs[id])}
70
+ />
71
+ </CSSTransition>
72
+ ) : null;
73
+ }
74
+ }
75
+
76
+ export default PopoverManager;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A state machine for the popover state. In particular, this class manages the
3
+ * mapping of parent nodes to their children, and translates touch events that
4
+ * traverse various nodes to actions that are conditioned on whether a popover
5
+ * is present.
6
+ */
7
+
8
+ class PopoverStateMachine {
9
+ constructor(handlers) {
10
+ this.handlers = handlers;
11
+
12
+ this.activePopover = null;
13
+ this.popovers = {};
14
+ }
15
+
16
+ /**
17
+ * Register a popover container as containing a set of children.
18
+ *
19
+ * @param {string} id - the identifier of the popover container
20
+ * @param {string[]} childIds - the identifiers of the nodes contained in
21
+ * the popover container
22
+ */
23
+ registerPopover(id, childIds) {
24
+ this.popovers[id] = childIds;
25
+ }
26
+
27
+ /**
28
+ * Unregister a popover container.
29
+ *
30
+ * @param {string} id - the identifier of the popover container to
31
+ * unregister
32
+ */
33
+ unregisterPopover(id) {
34
+ delete this.popovers[id];
35
+ }
36
+
37
+ /**
38
+ * @returns {boolean} - whether a popover is active and visible
39
+ */
40
+ isPopoverVisible() {
41
+ return this.activePopover != null;
42
+ }
43
+
44
+ /**
45
+ * Blur the active nodes.
46
+ */
47
+ onBlur() {
48
+ this.activePopover = null;
49
+ this.handlers.onActiveNodesChanged({
50
+ popover: null,
51
+ focus: null,
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Handle a focus event on the node with the given identifier.
57
+ *
58
+ * @param {string} id - the identifier of the node that was focused
59
+ */
60
+ onFocus(id) {
61
+ if (this.activePopover) {
62
+ // If we have a popover that is currently active, we focus this
63
+ // node if it's in the popover, and remove any highlight otherwise.
64
+ if (this._isNodeInsidePopover(this.activePopover, id)) {
65
+ this.handlers.onActiveNodesChanged({
66
+ popover: {
67
+ parentId: this.activePopover,
68
+ childIds: this.popovers[this.activePopover],
69
+ },
70
+ focus: id,
71
+ });
72
+ } else {
73
+ this.handlers.onActiveNodesChanged({
74
+ popover: {
75
+ parentId: this.activePopover,
76
+ childIds: this.popovers[this.activePopover],
77
+ },
78
+ focus: null,
79
+ });
80
+ }
81
+ } else {
82
+ this.activePopover = null;
83
+ this.handlers.onActiveNodesChanged({
84
+ popover: null,
85
+ focus: id,
86
+ });
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Handle a long press event on the node with the given identifier.
92
+ *
93
+ * @param {string} id - the identifier of the node that was long-pressed
94
+ */
95
+ onLongPress(id) {
96
+ // We only care about long presses if they occur on a popover, and we
97
+ // don't already have a popover active.
98
+ if (!this.activePopover && this.popovers[id]) {
99
+ // NOTE(charlie): There's an assumption here that focusing the
100
+ // first child is the correct behavior for a newly focused popover.
101
+ // This relies on the fact that the children are rendered
102
+ // bottom-up. If that rendering changes, this logic will need to
103
+ // change as well.
104
+ this.activePopover = id;
105
+ this.handlers.onActiveNodesChanged({
106
+ popover: {
107
+ parentId: this.activePopover,
108
+ childIds: this.popovers[this.activePopover],
109
+ },
110
+ focus: this._defaultNodeForPopover(this.activePopover),
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handle the trigger (click or hold) of the node with the given identifier.
117
+ *
118
+ * @param {string} id - the identifier of the node that was triggered
119
+ */
120
+ onTrigger(id) {
121
+ this.handlers.onClick(id, id, false);
122
+ }
123
+
124
+ /**
125
+ * Handle a touch-end event on the node with the given identifier.
126
+ *
127
+ * @param {string} id - the identifier of the node over which the touch
128
+ * ended
129
+ */
130
+ onTouchEnd(id) {
131
+ const inPopover = !!this.activePopover;
132
+ if (inPopover) {
133
+ // If we have a popover that is currently active, we trigger a
134
+ // click on this node if and only if it's in the popover, with the
135
+ // exception that, if the node passed back _is_ the active popover,
136
+ // then we trigger its default node. This latter case should only
137
+ // be triggered if the user were to tap down on a popover-enabled
138
+ // node, hold for long enough for the popover to appear, and then
139
+ // release without ever moving their finger, in which case, the
140
+ // underlying gesture system would have no idea that the popover's
141
+ // first child node was now focused.
142
+ if (this._isNodeInsidePopover(this.activePopover, id)) {
143
+ this.handlers.onClick(id, id, inPopover);
144
+ } else if (this.activePopover === id) {
145
+ const keyId = this._defaultNodeForPopover(id);
146
+ this.handlers.onClick(keyId, keyId, inPopover);
147
+ }
148
+ } else if (this.popovers[id]) {
149
+ // Otherwise, if the node is itself a popover revealer, trigger the
150
+ // clicking of its default node, but pass back the popover node ID
151
+ // for layout purposes.
152
+ const keyId = this._defaultNodeForPopover(id);
153
+ const domNodeId = id;
154
+ this.handlers.onClick(keyId, domNodeId, inPopover);
155
+ } else if (id != null) {
156
+ // Finally, if we have no active popover, and we touched up over a
157
+ // valid key, trigger a click.
158
+ this.onTrigger(id);
159
+ }
160
+
161
+ this.onBlur();
162
+ }
163
+
164
+ _isNodeInsidePopover(popover, id) {
165
+ return this.popovers[popover].includes(id);
166
+ }
167
+
168
+ _defaultNodeForPopover(popover) {
169
+ return this.popovers[popover][0];
170
+ }
171
+ }
172
+
173
+ export default PopoverStateMachine;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * React PropTypes that may be shared between components.
3
+ */
4
+
5
+ import PropTypes from "prop-types";
6
+
7
+ import {
8
+ BorderDirections,
9
+ EchoAnimationTypes,
10
+ IconTypes,
11
+ KeyTypes,
12
+ KeypadTypes,
13
+ } from "../consts.js";
14
+ import KeyConfigs from "../data/key-configs.js";
15
+
16
+ import * as CursorContexts from "./input/cursor-contexts.js";
17
+
18
+ export const iconPropType = PropTypes.shape({
19
+ type: PropTypes.oneOf(Object.keys(IconTypes)).isRequired,
20
+ data: PropTypes.string.isRequired,
21
+ });
22
+
23
+ export const keyIdPropType = PropTypes.oneOf(Object.keys(KeyConfigs));
24
+
25
+ export const keyConfigPropType = PropTypes.shape({
26
+ ariaLabel: PropTypes.string,
27
+ id: keyIdPropType.isRequired,
28
+ type: PropTypes.oneOf(Object.keys(KeyTypes)).isRequired,
29
+ childKeyIds: PropTypes.arrayOf(keyIdPropType),
30
+ icon: iconPropType.isRequired,
31
+ });
32
+
33
+ export const keypadConfigurationPropType = PropTypes.shape({
34
+ keypadType: PropTypes.oneOf(Object.keys(KeypadTypes)).isRequired,
35
+ extraKeys: PropTypes.arrayOf(keyIdPropType),
36
+ });
37
+
38
+ // NOTE(jared): This is no longer guaranteed to be React element
39
+ export const keypadElementPropType = PropTypes.shape({
40
+ activate: PropTypes.func.isRequired,
41
+ dismiss: PropTypes.func.isRequired,
42
+ configure: PropTypes.func.isRequired,
43
+ setCursor: PropTypes.func.isRequired,
44
+ setKeyHandler: PropTypes.func.isRequired,
45
+ getDOMNode: PropTypes.func.isRequired,
46
+ });
47
+
48
+ export const bordersPropType = PropTypes.arrayOf(
49
+ PropTypes.oneOf(Object.keys(BorderDirections)),
50
+ );
51
+
52
+ export const boundingBoxPropType = PropTypes.shape({
53
+ height: PropTypes.number,
54
+ width: PropTypes.number,
55
+ top: PropTypes.number,
56
+ right: PropTypes.number,
57
+ bottom: PropTypes.number,
58
+ left: PropTypes.number,
59
+ });
60
+
61
+ export const echoPropType = PropTypes.shape({
62
+ animationId: PropTypes.string.isRequired,
63
+ animationType: PropTypes.oneOf(Object.keys(EchoAnimationTypes)).isRequired,
64
+ borders: bordersPropType,
65
+ id: keyIdPropType.isRequired,
66
+ initialBounds: boundingBoxPropType.isRequired,
67
+ });
68
+
69
+ export const cursorContextPropType = PropTypes.oneOf(
70
+ Object.keys(CursorContexts),
71
+ );
72
+
73
+ export const popoverPropType = PropTypes.shape({
74
+ parentId: keyIdPropType.isRequired,
75
+ bounds: boundingBoxPropType.isRequired,
76
+ childKeyIds: PropTypes.arrayOf(keyIdPropType).isRequired,
77
+ });
78
+
79
+ export const childrenPropType = PropTypes.oneOfType([
80
+ PropTypes.arrayOf(PropTypes.node),
81
+ PropTypes.node,
82
+ ]);
@@ -0,0 +1,103 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ import * as React from "react";
4
+ import ReactDOM from "react-dom";
5
+ import {Provider} from "react-redux";
6
+
7
+ import {
8
+ activateKeypad,
9
+ dismissKeypad,
10
+ configureKeypad,
11
+ setCursor,
12
+ setKeyHandler,
13
+ } from "../actions/index.js";
14
+ import {createStore} from "../store/index.js";
15
+
16
+ import KeypadContainer from "./keypad-container.js";
17
+
18
+ import type {CSSProperties} from "aphrodite";
19
+
20
+ type Props = {|
21
+ onElementMounted?: ($FlowFixMe) => void,
22
+ onDismiss?: () => mixed,
23
+ style?: CSSProperties,
24
+ |};
25
+
26
+ class ProvidedKeypad extends React.Component<Props> {
27
+ mounted: boolean;
28
+ store: $FlowFixMe;
29
+
30
+ UNSAFE_componentWillMount() {
31
+ this.store = createStore();
32
+ }
33
+
34
+ componentDidMount() {
35
+ this.mounted = true;
36
+ }
37
+
38
+ componentWillUnmount() {
39
+ this.mounted = false;
40
+ }
41
+
42
+ activate: () => void = () => {
43
+ this.store.dispatch(activateKeypad());
44
+ };
45
+
46
+ dismiss: () => void = () => {
47
+ this.store.dispatch(dismissKeypad());
48
+ };
49
+
50
+ configure: () => void = (configuration, cb) => {
51
+ this.store.dispatch(configureKeypad(configuration));
52
+
53
+ // HACK(charlie): In Perseus, triggering a focus causes the keypad to
54
+ // animate into view and re-configure. We'd like to provide the option
55
+ // to re-render the re-configured keypad before animating it into view,
56
+ // to avoid jank in the animation. As such, we support passing a
57
+ // callback into `configureKeypad`. However, implementing this properly
58
+ // would require middleware, etc., so we just hack it on with
59
+ // `setTimeout` for now.
60
+ setTimeout(() => cb && cb());
61
+ };
62
+
63
+ setCursor: () => void = (cursor) => {
64
+ this.store.dispatch(setCursor(cursor));
65
+ };
66
+
67
+ setKeyHandler: (keyHandler: $FlowFixMe) => void = (keyHandler) => {
68
+ this.store.dispatch(setKeyHandler(keyHandler));
69
+ };
70
+
71
+ getDOMNode: () => $Call<typeof ReactDOM.findDOMNode, any> = () => {
72
+ return ReactDOM.findDOMNode(this);
73
+ };
74
+
75
+ render(): React.Node {
76
+ const {onElementMounted, ...rest} = this.props;
77
+
78
+ return (
79
+ <Provider store={this.store}>
80
+ <KeypadContainer
81
+ onElementMounted={(element) => {
82
+ // Append the dispatch methods that we want to expose
83
+ // externally to the returned React element.
84
+ const elementWithDispatchMethods = {
85
+ ...element,
86
+ activate: this.activate,
87
+ dismiss: this.dismiss,
88
+ configure: this.configure,
89
+ setCursor: this.setCursor,
90
+ setKeyHandler: this.setKeyHandler,
91
+ getDOMNode: this.getDOMNode,
92
+ };
93
+ onElementMounted &&
94
+ onElementMounted(elementWithDispatchMethods);
95
+ }}
96
+ {...rest}
97
+ />
98
+ </Provider>
99
+ );
100
+ }
101
+ }
102
+
103
+ export default ProvidedKeypad;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Common styles shared across components.
3
+ */
4
+
5
+ import {StyleSheet} from "aphrodite";
6
+
7
+ import {compactKeypadBorderRadiusPx} from "./common-style.js";
8
+
9
+ export default StyleSheet.create({
10
+ row: {
11
+ flexDirection: "row",
12
+ },
13
+ column: {
14
+ flexDirection: "column",
15
+ },
16
+ oneColumn: {
17
+ flexGrow: 1,
18
+ },
19
+ fullWidth: {
20
+ width: "100%",
21
+ },
22
+ stretch: {
23
+ alignItems: "stretch",
24
+ },
25
+ centered: {
26
+ justifyContent: "center",
27
+ alignItems: "center",
28
+ },
29
+ centeredText: {
30
+ textAlign: "center",
31
+ },
32
+ roundedTopLeft: {
33
+ borderTopLeftRadius: compactKeypadBorderRadiusPx,
34
+ },
35
+ roundedTopRight: {
36
+ borderTopRightRadius: compactKeypadBorderRadiusPx,
37
+ },
38
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A component that renders a single SVG icon.
3
+ */
4
+
5
+ import PropTypes from "prop-types";
6
+ import * as React from "react";
7
+
8
+ import * as Iconography from "./iconography/index.js";
9
+
10
+ class SvgIcon extends React.Component {
11
+ static propTypes = {
12
+ color: PropTypes.string.isRequired,
13
+ name: PropTypes.string.isRequired,
14
+ };
15
+
16
+ render() {
17
+ const {color, name} = this.props;
18
+
19
+ // eslint-disable-next-line import/namespace
20
+ const SvgForName = Iconography[name];
21
+ return <SvgForName color={color} />;
22
+ }
23
+ }
24
+
25
+ export default SvgIcon;
@@ -0,0 +1,65 @@
1
+ import {mount} from "enzyme";
2
+ import * as React from "react";
3
+
4
+ import Tabbar from "../tabbar.js";
5
+
6
+ describe("<Tabbar />", () => {
7
+ it("defaults to selecting the first item", () => {
8
+ // Arrange
9
+ const wrapper = mount(
10
+ <Tabbar
11
+ items={["Numbers", "Geometry", "Operators"]}
12
+ onSelect={() => {}}
13
+ />,
14
+ );
15
+
16
+ // Assert
17
+ expect(wrapper).toHaveState("selectedItem", 0);
18
+ const firstItem = wrapper.find("TabbarItem").first();
19
+ expect(firstItem).toHaveProp("itemState", "active");
20
+ });
21
+
22
+ it("selects the second item", () => {
23
+ // Arrange
24
+ const wrapper = mount(
25
+ <Tabbar
26
+ items={["Numbers", "Geometry", "Operators"]}
27
+ onSelect={() => {}}
28
+ />,
29
+ );
30
+
31
+ // Act
32
+ let secondItem = wrapper.find("TabbarItem").at(1);
33
+ secondItem.simulate("click");
34
+
35
+ // Assert
36
+ expect(wrapper).toHaveState("selectedItem", 1);
37
+ const firstItem = wrapper.find("TabbarItem").at(0);
38
+ expect(firstItem).toHaveProp("itemState", "inactive");
39
+ // NOTE: we have to re-get the second item to get it's updated state
40
+ secondItem = wrapper.find("TabbarItem").at(1);
41
+ expect(secondItem).toHaveProp("itemState", "active");
42
+ });
43
+
44
+ it("tapping an already selected item doesn't change selection", () => {
45
+ // Arrange
46
+ const wrapper = mount(
47
+ <Tabbar
48
+ items={["Numbers", "Geometry", "Operators"]}
49
+ onSelect={() => {}}
50
+ />,
51
+ );
52
+
53
+ // Assert
54
+ expect(wrapper).toHaveState("selectedItem", 0);
55
+ const firstItem = wrapper.find("TabbarItem").first();
56
+ expect(firstItem).toHaveProp("itemState", "active");
57
+
58
+ // Act
59
+ firstItem.simulate("click");
60
+
61
+ // Assert
62
+ expect(wrapper).toHaveState("selectedItem", 0);
63
+ expect(firstItem).toHaveProp("itemState", "active");
64
+ });
65
+ });