@khanacademy/math-input 16.1.2 → 16.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/CHANGELOG.md +8 -0
- package/dist/components/input/math-input.d.ts +4 -6
- package/dist/es/index.js +78 -45
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +73 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/integration.test.tsx +17 -1
- package/src/components/input/math-input.tsx +84 -51
- package/tsconfig-build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -99,7 +99,7 @@ describe("math input integration", () => {
|
|
|
99
99
|
).not.toBeInTheDocument();
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it("shows the keypad after input interaction", async () => {
|
|
102
|
+
it("shows the keypad after input touch-interaction", async () => {
|
|
103
103
|
render(<ConnectedMathInput />);
|
|
104
104
|
|
|
105
105
|
const input = screen.getByLabelText(
|
|
@@ -115,6 +115,22 @@ describe("math input integration", () => {
|
|
|
115
115
|
expect(screen.getByRole("button", {name: "1"})).toBeVisible();
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
it("shows the keypad after input click-interaction", async () => {
|
|
119
|
+
render(<ConnectedMathInput />);
|
|
120
|
+
|
|
121
|
+
const input = screen.getByLabelText(
|
|
122
|
+
"Math input box Tap with one or two fingers to open keyboard",
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
userEvent.click(input);
|
|
126
|
+
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(screen.getByRole("button", {name: "4"})).toBeVisible();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(screen.getByRole("button", {name: "1"})).toBeVisible();
|
|
132
|
+
});
|
|
133
|
+
|
|
118
134
|
it("updates input when using keypad", async () => {
|
|
119
135
|
render(<ConnectedMathInput />);
|
|
120
136
|
|
|
@@ -56,6 +56,8 @@ class MathInput extends React.Component<Props, State> {
|
|
|
56
56
|
recordTouchStartOutside: (arg1: any) => void;
|
|
57
57
|
// @ts-expect-error - TS2564 - Property 'blurOnTouchEndOutside' has no initializer and is not definitely assigned in the constructor.
|
|
58
58
|
blurOnTouchEndOutside: (arg1: any) => void;
|
|
59
|
+
// @ts-expect-error - TS2564 - Property 'blurOnClickOutside' has no initializer and is not definitely assigned in the constructor.
|
|
60
|
+
blurOnClickOutside: (arg1: any) => void;
|
|
59
61
|
dragListener: any;
|
|
60
62
|
inputRef: HTMLDivElement | null | undefined;
|
|
61
63
|
_isMounted: boolean | null | undefined;
|
|
@@ -65,7 +67,6 @@ class MathInput extends React.Component<Props, State> {
|
|
|
65
67
|
_root: any;
|
|
66
68
|
// @ts-expect-error - TS2564 - Property '_containerBounds' has no initializer and is not definitely assigned in the constructor.
|
|
67
69
|
_containerBounds: ClientRect;
|
|
68
|
-
_keypadBounds: ClientRect | null | undefined;
|
|
69
70
|
|
|
70
71
|
static defaultProps: DefaultProps = {
|
|
71
72
|
style: {},
|
|
@@ -120,6 +121,24 @@ class MathInput extends React.Component<Props, State> {
|
|
|
120
121
|
this._root = this._container.querySelector(".mq-root-block");
|
|
121
122
|
this._root.addEventListener("scroll", this._handleScroll);
|
|
122
123
|
|
|
124
|
+
const isWithinKeypadBounds = (x: number, y: number): boolean => {
|
|
125
|
+
const bounds = this._getKeypadBounds();
|
|
126
|
+
|
|
127
|
+
// If there are no bounds, then the keypad is not mounted, so we
|
|
128
|
+
// assume that the event is not within the keypad bounds.
|
|
129
|
+
if (!bounds) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
(bounds.left <= x &&
|
|
135
|
+
bounds.right >= x &&
|
|
136
|
+
bounds.top <= y &&
|
|
137
|
+
bounds.bottom >= y) ||
|
|
138
|
+
bounds.bottom < y
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
123
142
|
// Record the initial scroll displacement on touch start. This allows
|
|
124
143
|
// us to detect whether a touch event was a scroll and only blur the
|
|
125
144
|
// input on non-scrolls--blurring the input on scroll makes for a
|
|
@@ -139,19 +158,12 @@ class MathInput extends React.Component<Props, State> {
|
|
|
139
158
|
this.props.keypadElement &&
|
|
140
159
|
this.props.keypadElement.getDOMNode()
|
|
141
160
|
) {
|
|
142
|
-
const bounds = this._getKeypadBounds();
|
|
143
161
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
|
144
162
|
const [x, y] = [
|
|
145
163
|
evt.changedTouches[i].clientX,
|
|
146
164
|
evt.changedTouches[i].clientY,
|
|
147
165
|
];
|
|
148
|
-
if (
|
|
149
|
-
(bounds.left <= x &&
|
|
150
|
-
bounds.right >= x &&
|
|
151
|
-
bounds.top <= y &&
|
|
152
|
-
bounds.bottom >= y) ||
|
|
153
|
-
bounds.bottom < y
|
|
154
|
-
) {
|
|
166
|
+
if (isWithinKeypadBounds(x, y)) {
|
|
155
167
|
touchDidStartInOrBelowKeypad = true;
|
|
156
168
|
break;
|
|
157
169
|
}
|
|
@@ -194,28 +206,34 @@ class MathInput extends React.Component<Props, State> {
|
|
|
194
206
|
}
|
|
195
207
|
};
|
|
196
208
|
|
|
209
|
+
// We want to allow the user to blur the input by clicking outside of it
|
|
210
|
+
// when using ChromeOS third-party browsers that use mobile user agents,
|
|
211
|
+
// but don't actually simulate touch events.
|
|
212
|
+
this.blurOnClickOutside = (evt: any) => {
|
|
213
|
+
if (this.state.focused) {
|
|
214
|
+
if (!this._container.contains(evt.target)) {
|
|
215
|
+
if (
|
|
216
|
+
this.props.keypadElement &&
|
|
217
|
+
this.props.keypadElement.getDOMNode()
|
|
218
|
+
) {
|
|
219
|
+
const [x, y] = [evt.clientX, evt.clientY];
|
|
220
|
+
// We only want to blur if the click is above the keypad,
|
|
221
|
+
// to the left of the keypad, or to the right of the keypad.
|
|
222
|
+
// The reasoning for not blurring for any clicks below the keypad is
|
|
223
|
+
// that the keypad may be anchored above the 'Check answer' bottom bar,
|
|
224
|
+
// in which case we don't want to dismiss the keypad on check.
|
|
225
|
+
if (!isWithinKeypadBounds(x, y)) {
|
|
226
|
+
this.blur();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
197
233
|
window.addEventListener("touchstart", this.recordTouchStartOutside);
|
|
198
234
|
window.addEventListener("touchend", this.blurOnTouchEndOutside);
|
|
199
235
|
window.addEventListener("touchcancel", this.blurOnTouchEndOutside);
|
|
200
|
-
|
|
201
|
-
// HACK(benkomalo): if the window resizes, the keypad bounds can
|
|
202
|
-
// change. That's a bit peeking into the internals of the keypad
|
|
203
|
-
// itself, since we know bounds can change only when the viewport
|
|
204
|
-
// changes, but seems like a rare enough thing to get wrong that it's
|
|
205
|
-
// not worth wiring up extra things for the technical "purity" of
|
|
206
|
-
// having the keypad notify of changes to us.
|
|
207
|
-
window.addEventListener("resize", this._clearKeypadBoundsCache);
|
|
208
|
-
window.addEventListener(
|
|
209
|
-
"orientationchange",
|
|
210
|
-
this._clearKeypadBoundsCache,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// eslint-disable-next-line react/no-unsafe
|
|
215
|
-
UNSAFE_componentWillReceiveProps(props: Props) {
|
|
216
|
-
if (this.props.keypadElement !== props.keypadElement) {
|
|
217
|
-
this._clearKeypadBoundsCache();
|
|
218
|
-
}
|
|
236
|
+
window.addEventListener("click", this.blurOnClickOutside);
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
|
@@ -234,23 +252,9 @@ class MathInput extends React.Component<Props, State> {
|
|
|
234
252
|
window.removeEventListener("touchstart", this.recordTouchStartOutside);
|
|
235
253
|
window.removeEventListener("touchend", this.blurOnTouchEndOutside);
|
|
236
254
|
window.removeEventListener("touchcancel", this.blurOnTouchEndOutside);
|
|
237
|
-
|
|
238
|
-
window.removeEventListener("resize", this._clearKeypadBoundsCache());
|
|
239
|
-
window.removeEventListener(
|
|
240
|
-
"orientationchange",
|
|
241
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
242
|
-
this._clearKeypadBoundsCache(),
|
|
243
|
-
);
|
|
255
|
+
window.removeEventListener("click", this.blurOnClickOutside);
|
|
244
256
|
}
|
|
245
257
|
|
|
246
|
-
_clearKeypadBoundsCache: () => void = () => {
|
|
247
|
-
this._keypadBounds = null;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
_cacheKeypadBounds: (arg1: any) => void = (keypadNode) => {
|
|
251
|
-
this._keypadBounds = keypadNode.getBoundingClientRect();
|
|
252
|
-
};
|
|
253
|
-
|
|
254
258
|
_updateInputPadding: () => void = () => {
|
|
255
259
|
this._container = ReactDOM.findDOMNode(this) as HTMLDivElement;
|
|
256
260
|
this._root = this._container.querySelector(".mq-root-block");
|
|
@@ -263,14 +267,17 @@ class MathInput extends React.Component<Props, State> {
|
|
|
263
267
|
this._root.style.fontSize = `${fontSizePt}pt`;
|
|
264
268
|
};
|
|
265
269
|
|
|
266
|
-
/**
|
|
267
|
-
_getKeypadBounds
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
/** Returns the current bounds of the keypadElement */
|
|
271
|
+
_getKeypadBounds(): DOMRect | null {
|
|
272
|
+
const keypadNode = this.props.keypadElement?.getDOMNode();
|
|
273
|
+
|
|
274
|
+
// If the keypad is mounted, return its bounds. Otherwise, return null.
|
|
275
|
+
if (keypadNode instanceof Element) {
|
|
276
|
+
return keypadNode.getBoundingClientRect();
|
|
271
277
|
}
|
|
272
|
-
|
|
273
|
-
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
274
281
|
|
|
275
282
|
_updateCursorHandle: (arg1?: boolean) => void = (animateIntoPosition) => {
|
|
276
283
|
const containerBounds = this._container.getBoundingClientRect();
|
|
@@ -624,6 +631,32 @@ class MathInput extends React.Component<Props, State> {
|
|
|
624
631
|
}
|
|
625
632
|
};
|
|
626
633
|
|
|
634
|
+
// We want to allow the user to be able to focus the input via click
|
|
635
|
+
// when using ChromeOS third-party browsers that use mobile user agents,
|
|
636
|
+
// but don't actually simulate touch events.
|
|
637
|
+
handleClick = (e: React.MouseEvent<HTMLDivElement>): void => {
|
|
638
|
+
e.stopPropagation();
|
|
639
|
+
|
|
640
|
+
// Hide the cursor handle on click
|
|
641
|
+
this._hideCursorHandle();
|
|
642
|
+
|
|
643
|
+
// Cache the container bounds, so as to avoid re-computing. If we don't
|
|
644
|
+
// have any content, then it's not necessary, since the cursor can't be
|
|
645
|
+
// moved anyway.
|
|
646
|
+
if (this.mathField.getContent() !== "") {
|
|
647
|
+
this._containerBounds = this._container.getBoundingClientRect();
|
|
648
|
+
|
|
649
|
+
// Make the cursor visible and set the handle-less cursor's
|
|
650
|
+
// location.
|
|
651
|
+
this._insertCursorAtClosestNode(e.clientX, e.clientY);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Trigger a focus event, if we're not already focused.
|
|
655
|
+
if (!this.state.focused) {
|
|
656
|
+
this.focus();
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
627
660
|
handleTouchMove: (arg1: React.TouchEvent<HTMLDivElement>) => void = (e) => {
|
|
628
661
|
e.stopPropagation();
|
|
629
662
|
|
|
@@ -898,7 +931,7 @@ class MathInput extends React.Component<Props, State> {
|
|
|
898
931
|
onTouchStart={this.handleTouchStart}
|
|
899
932
|
onTouchMove={this.handleTouchMove}
|
|
900
933
|
onTouchEnd={this.handleTouchEnd}
|
|
901
|
-
onClick={
|
|
934
|
+
onClick={this.handleClick}
|
|
902
935
|
role={"textbox"}
|
|
903
936
|
ariaLabel={ariaLabel}
|
|
904
937
|
>
|