@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.
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/{build/math-input.css → dist/es/index.css} +0 -150
- package/dist/es/index.js +7798 -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 +7768 -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 +60 -89
- 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 +29 -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 +103 -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 +14 -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,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
|
+
});
|