@khanacademy/math-input 14.1.0 → 14.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Khan Academy's new expression editor for the mobile web.",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "14.1.0",
6
+ "version": "14.2.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -20,8 +20,8 @@
20
20
  "source": "src/index.ts",
21
21
  "scripts": {},
22
22
  "dependencies": {
23
- "@khanacademy/perseus-core": "1.1.1",
24
- "mathquill": "git+https://git@github.com/Khan/mathquill.git#32d9f351aaa68537170b3120a52e99b8def3a2c3",
23
+ "@khanacademy/perseus-core": "1.1.2",
24
+ "mathquill": "git+https://git@github.com/Khan/mathquill.git#48410e80d760bbd5105544d4a4ab459a28dc2cbc",
25
25
  "performance-now": "^0.2.0"
26
26
  },
27
27
  "devDependencies": {
@@ -30,6 +30,7 @@
30
30
  "@khanacademy/wonder-blocks-core": "^6.0.0",
31
31
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
32
32
  "@khanacademy/wonder-blocks-popover": "^2.0.11",
33
+ "@khanacademy/wonder-blocks-timing": "^4.0.2",
33
34
  "@khanacademy/wonder-stuff-core": "^1.5.1",
34
35
  "aphrodite": "^1.1.0",
35
36
  "jquery": "^2.1.1",
@@ -49,6 +50,7 @@
49
50
  "@khanacademy/wonder-blocks-color": "^2.0.1",
50
51
  "@khanacademy/wonder-blocks-core": "^6.0.0",
51
52
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
53
+ "@khanacademy/wonder-blocks-timing": "^4.0.2",
52
54
  "@khanacademy/wonder-stuff-core": "^1.5.1",
53
55
  "aphrodite": "^1.1.0",
54
56
  "jquery": "^2.1.1",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Aphrodite doesn't play well with CSSTransition from react-transition-group,
3
+ * which assumes that you have CSS classes and it can combine them arbitrarily.
4
+ *
5
+ * There are also some issue with react-transition-group that make it difficult
6
+ * to work. Even if the CSS classes are defined ahead of time it makes no
7
+ * guarantee that the start style will be applied by the browser before the
8
+ * active style is applied. This can cause the first time a transition runs to
9
+ * fail.
10
+ *
11
+ * AphroditeCSSTransitionGroup provides a wrapper around TransitionGroup to
12
+ * address these issues.
13
+ *
14
+ * There are three types of transitions:
15
+ * - appear: the time the child is added to the render tree
16
+ * - enter: whenever the child is added to the render tree after "appear". If
17
+ * no "appear" transition is specified then the "enter" transition will also
18
+ * be used for the first time the child is added to the render tree.
19
+ * - leave: whenever the child is removed from the render tree
20
+ *
21
+ * Each transition type has two states:
22
+ * - base: e.g. css(enter)
23
+ * - active: e.g. css(enter, enterActive)
24
+ *
25
+ * If "done" styles are not provided, the "active" style will remain on the
26
+ * component after the animation has completed.
27
+ *
28
+ * Usage: TBD
29
+ *
30
+ * Limitations:
31
+ * - This component only supports a single child whereas TransitionGroup supports
32
+ * multiple children.
33
+ * - We ignore inline styles that are provided as part of AnimationStyles.
34
+ *
35
+ * TODOs:
36
+ * - (FEI-3211): Change the API for AphroditeCSSTransitionGroup so that it makes
37
+ * bad states impossible.
38
+ */
39
+ import * as React from "react";
40
+ import {TransitionGroup} from "react-transition-group";
41
+
42
+ import TransitionChild from "./transition-child";
43
+
44
+ import type {AnimationStyles} from "./types";
45
+
46
+ type Props = {
47
+ // If a function is provided, that function will be called to retrieve the
48
+ // current set of animation styles to be used when animating the children.
49
+ transitionStyle: AnimationStyles | (() => AnimationStyles);
50
+ transitionAppearTimeout?: number;
51
+ transitionEnterTimeout?: number;
52
+ transitionLeaveTimeout?: number;
53
+ children?: React.ReactNode;
54
+ };
55
+
56
+ class AphroditeCSSTransitionGroup extends React.Component<Props> {
57
+ render(): React.ReactNode {
58
+ const {children} = this.props;
59
+ return (
60
+ // `component={null}` prevents wrapping each child with a <div>
61
+ // which can muck with certain layouts.
62
+ <TransitionGroup component={null}>
63
+ {React.Children.map(children, (child) => (
64
+ <TransitionChild
65
+ transitionStyles={this.props.transitionStyle}
66
+ appearTimeout={this.props.transitionAppearTimeout}
67
+ enterTimeout={this.props.transitionEnterTimeout}
68
+ leaveTimeout={this.props.transitionLeaveTimeout}
69
+ >
70
+ {child}
71
+ </TransitionChild>
72
+ ))}
73
+ </TransitionGroup>
74
+ );
75
+ }
76
+ }
77
+
78
+ export default AphroditeCSSTransitionGroup;
@@ -0,0 +1,191 @@
1
+ import {withActionScheduler} from "@khanacademy/wonder-blocks-timing";
2
+ import * as React from "react";
3
+ import ReactDOM from "react-dom";
4
+
5
+ import {processStyleType} from "./util";
6
+
7
+ import type {AnimationStyles} from "./types";
8
+ import type {WithActionSchedulerProps} from "@khanacademy/wonder-blocks-timing";
9
+
10
+ type ChildProps = {
11
+ transitionStyles: AnimationStyles | (() => AnimationStyles);
12
+ appearTimeout?: number; // default appearTimeout to be the same as enterTimeout
13
+ enterTimeout?: number;
14
+ leaveTimeout?: number;
15
+ children: React.ReactNode;
16
+ in?: boolean; // provided by TransitionGroup
17
+ } & WithActionSchedulerProps;
18
+
19
+ type ChildState = {
20
+ // Keeps track of whether we should render our children or not.
21
+ status: "mounted" | "unmounted";
22
+ };
23
+
24
+ class TransitionChild extends React.Component<ChildProps, ChildState> {
25
+ // Each 2-tuple in the queue represents two classnames: one to remove and
26
+ // one to add (in that order).
27
+ classNameQueue: Array<[string, string]>;
28
+ // We keep track of all of the current applied classes so that we can remove
29
+ // them before a new transition starts in the case of the current transition
30
+ // being interrupted.
31
+ appliedClassNames: Set<string>;
32
+ _isMounted = false;
33
+
34
+ // The use of getDerivedStateFromProps here is to avoid an extra call to
35
+ // setState if the component re-enters. This can happen if TransitionGroup
36
+ // sets `in` from `false` to `true`.
37
+ // eslint-disable-next-line no-restricted-syntax
38
+ static getDerivedStateFromProps(
39
+ {in: nextIn}: ChildProps,
40
+ prevState: ChildState,
41
+ ): Partial<ChildState> | null {
42
+ if (nextIn && prevState.status === "unmounted") {
43
+ return {status: "mounted"};
44
+ }
45
+ return null;
46
+ }
47
+
48
+ constructor(props: ChildProps) {
49
+ super(props);
50
+
51
+ this._isMounted = false;
52
+ this.classNameQueue = [];
53
+ this.appliedClassNames = new Set();
54
+
55
+ this.state = {
56
+ status: "mounted",
57
+ };
58
+ }
59
+
60
+ componentDidMount() {
61
+ this._isMounted = true;
62
+
63
+ if (typeof this.props.appearTimeout === "number") {
64
+ this.transition("appear", this.props.appearTimeout);
65
+ } else {
66
+ this.transition("enter", this.props.enterTimeout);
67
+ }
68
+ }
69
+
70
+ componentDidUpdate(oldProps: ChildProps, oldState: ChildState) {
71
+ if (oldProps.in && !this.props.in) {
72
+ this.transition("leave", this.props.leaveTimeout);
73
+ } else if (!oldProps.in && this.props.in) {
74
+ this.transition("enter", this.props.enterTimeout);
75
+ }
76
+
77
+ if (oldState.status !== "mounted" && this.state.status === "mounted") {
78
+ // Remove the node from the DOM
79
+ // eslint-disable-next-line react/no-did-update-set-state
80
+ this.setState({status: "unmounted"});
81
+ }
82
+ }
83
+
84
+ // NOTE: This will only get called when the parent TransitionGroup becomes
85
+ // unmounted. This is because that component clones all of its children and
86
+ // keeps them around so that they can be animated when leaving and also so
87
+ // that the can be animated when re-rentering if that occurs.
88
+ componentWillUnmount() {
89
+ this._isMounted = false;
90
+ this.props.schedule.clearAll();
91
+ }
92
+
93
+ removeAllClasses(node: Element) {
94
+ for (const className of this.appliedClassNames) {
95
+ this.removeClass(node, className);
96
+ }
97
+ }
98
+
99
+ addClass = (elem: Element, className: string): void => {
100
+ if (className) {
101
+ elem.classList.add(className);
102
+ this.appliedClassNames.add(className);
103
+ }
104
+ };
105
+
106
+ removeClass = (elem: Element, className: string): void => {
107
+ if (className) {
108
+ elem.classList.remove(className);
109
+ this.appliedClassNames.delete(className);
110
+ }
111
+ };
112
+
113
+ transition(
114
+ animationType: "appear" | "enter" | "leave",
115
+ duration?: number | null,
116
+ ) {
117
+ const node = ReactDOM.findDOMNode(this);
118
+
119
+ if (!(node instanceof Element)) {
120
+ return;
121
+ }
122
+
123
+ // Remove any classes from previous transitions.
124
+ this.removeAllClasses(node);
125
+
126
+ // A previous transition may still be in progress so clear its timers.
127
+ this.props.schedule.clearAll();
128
+
129
+ const transitionStyles =
130
+ typeof this.props.transitionStyles === "function"
131
+ ? this.props.transitionStyles()
132
+ : this.props.transitionStyles;
133
+
134
+ const {className} = processStyleType(transitionStyles[animationType]);
135
+ const {className: activeClassName} = processStyleType([
136
+ transitionStyles[animationType],
137
+ transitionStyles[animationType + "Active"],
138
+ ]);
139
+
140
+ // Put the node in the starting position.
141
+ this.addClass(node, className);
142
+
143
+ // Queue the component to show the "active" style.
144
+ this.queueClass(className, activeClassName);
145
+
146
+ // Unmount the children after the 'leave' transition has completed.
147
+ if (animationType === "leave") {
148
+ this.props.schedule.timeout(() => {
149
+ if (this._isMounted) {
150
+ this.setState({status: "unmounted"});
151
+ }
152
+ }, duration || 0);
153
+ }
154
+ }
155
+
156
+ queueClass(removeClassName: string, addClassName: string) {
157
+ this.classNameQueue.push([removeClassName, addClassName]);
158
+ this.props.schedule.animationFrame(this.flushClassNameQueue);
159
+ }
160
+
161
+ flushClassNameQueue = () => {
162
+ if (this._isMounted) {
163
+ const node = ReactDOM.findDOMNode(this);
164
+ if (node instanceof Element) {
165
+ this.classNameQueue.forEach(
166
+ ([removeClassName, addClassName]: [any, any]) => {
167
+ // Remove the old class before adding a new class just
168
+ // in case the new class is the same as the old one.
169
+ this.removeClass(node, removeClassName);
170
+ this.addClass(node, addClassName);
171
+ },
172
+ );
173
+ }
174
+ }
175
+
176
+ // Remove all items in the Array.
177
+ this.classNameQueue.length = 0;
178
+ };
179
+
180
+ render(): React.ReactNode {
181
+ const {status} = this.state;
182
+
183
+ if (status === "unmounted") {
184
+ return null;
185
+ }
186
+
187
+ return this.props.children;
188
+ }
189
+ }
190
+
191
+ export default withActionScheduler(TransitionChild);
@@ -0,0 +1,20 @@
1
+ import type {StyleType} from "@khanacademy/wonder-blocks-core";
2
+ import type {CSSProperties} from "aphrodite";
3
+
4
+ export type AnimationStyles = {
5
+ enter?: StyleType;
6
+ enterActive?: StyleType;
7
+ leave?: StyleType;
8
+ leaveActive?: StyleType;
9
+ appear?: StyleType;
10
+ appearActive?: StyleType;
11
+ };
12
+
13
+ export type InAnimationStyles = {
14
+ enter?: CSSProperties;
15
+ enterActive?: CSSProperties;
16
+ leave?: CSSProperties;
17
+ leaveActive?: CSSProperties;
18
+ appear?: CSSProperties;
19
+ appearActive?: CSSProperties;
20
+ };
@@ -0,0 +1,97 @@
1
+ import {entries} from "@khanacademy/wonder-stuff-core";
2
+ import {StyleSheet, css} from "aphrodite";
3
+
4
+ import type {InAnimationStyles} from "./types";
5
+ import type {StyleType} from "@khanacademy/wonder-blocks-core";
6
+ import type {CSSProperties} from "aphrodite";
7
+
8
+ function flatten(list?: StyleType): ReadonlyArray<CSSProperties> {
9
+ const result: Array<CSSProperties> = [];
10
+
11
+ if (!list) {
12
+ return result;
13
+ }
14
+ if (Array.isArray(list)) {
15
+ for (const item of list) {
16
+ result.push(...flatten(item));
17
+ }
18
+ } else {
19
+ result.push(list as any);
20
+ }
21
+
22
+ return result;
23
+ }
24
+
25
+ export function processStyleType(style?: StyleType): {
26
+ className: string;
27
+ style: Record<any, any>;
28
+ } {
29
+ const stylesheetStyles: Array<CSSProperties> = [];
30
+ const inlineStyles: Array<CSSProperties> = [];
31
+
32
+ if (!style) {
33
+ return {
34
+ style: {},
35
+ className: "",
36
+ };
37
+ }
38
+
39
+ // Check to see if we should inline all the styles for snapshot tests.
40
+ const shouldInlineStyles =
41
+ typeof globalThis !== "undefined" &&
42
+ globalThis.SNAPSHOT_INLINE_APHRODITE;
43
+
44
+ flatten(style).forEach((child) => {
45
+ // Check for aphrodite internal property
46
+ const _definition = (child as any)._definition;
47
+ if (_definition != null) {
48
+ if (shouldInlineStyles) {
49
+ const def: Record<string, any> = {};
50
+ // React 16 complains about invalid keys in inline styles.
51
+ // It doesn't accept kebab-case in media queries and instead
52
+ // prefers camelCase.
53
+ for (const [key, value] of entries(_definition)) {
54
+ // This regex converts all instances of -{lowercaseLetter}
55
+ // to the uppercase version of that letter, without the
56
+ // leading dash.
57
+ def[
58
+ key.replace(/-[a-z]/g, (match) =>
59
+ match[1].toUpperCase(),
60
+ )
61
+ ] = value;
62
+ }
63
+ inlineStyles.push(def);
64
+ } else {
65
+ stylesheetStyles.push(child);
66
+ }
67
+ } else {
68
+ inlineStyles.push(child);
69
+ }
70
+ });
71
+
72
+ const inlineStylesObject = Object.assign({}, ...inlineStyles);
73
+
74
+ // TODO(somewhatabstract): When aphrodite no longer puts "!important" on
75
+ // all the styles, remove this <ADD JIRA ISSUE HERE IF THIS PASSES REVIEW>
76
+ // If we're not snapshotting styles, let's create a class for the inline
77
+ // styles so that they can apply to the element even with aphrodite's
78
+ // use of !important.
79
+ if (inlineStyles.length > 0 && !shouldInlineStyles) {
80
+ const inlineStylesStyleSheet = StyleSheet.create({
81
+ inlineStyles: inlineStylesObject,
82
+ });
83
+ stylesheetStyles.push(inlineStylesStyleSheet.inlineStyles);
84
+ }
85
+
86
+ return {
87
+ style: shouldInlineStyles ? inlineStylesObject : {},
88
+ className: css(...stylesheetStyles),
89
+ };
90
+ }
91
+
92
+ export const createTransition = (styles: InAnimationStyles) => {
93
+ // NOTE(kevinb): TypeScript infers the optional properties on `InAnimationStyles`
94
+ // as `CSSProperties | undefined`. This is not compatible with `StyleSheet.create`
95
+ // which expects `CSSProperties` on the object that's passed in to it.
96
+ return StyleSheet.create(styles as any);
97
+ };