@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/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": "16.1.2",
6
+ "version": "16.2.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -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
- // @ts-expect-error - TS2769 - No overload matches this call.
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
- /** Gets and cache they bounds of the keypadElement */
267
- _getKeypadBounds: () => any = () => {
268
- if (!this._keypadBounds) {
269
- const node = this.props.keypadElement?.getDOMNode();
270
- this._cacheKeypadBounds(node);
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
- return this._keypadBounds;
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={(e) => e.stopPropagation()}
934
+ onClick={this.handleClick}
902
935
  role={"textbox"}
903
936
  ariaLabel={ariaLabel}
904
937
  >