@pie-lib/graphing 4.0.23 → 4.0.24-beta.10

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 (69) hide show
  1. package/CHANGELOG.md +0 -92
  2. package/lib/__tests__/bg.test.js +282 -0
  3. package/lib/__tests__/coordinates-label.test.js +276 -0
  4. package/lib/__tests__/graph-with-controls.test.js +275 -0
  5. package/lib/__tests__/graph.test.js +927 -0
  6. package/lib/__tests__/grid-setup.test.js +729 -0
  7. package/lib/__tests__/grid.test.js +29 -0
  8. package/lib/__tests__/key-legend.test.js +321 -0
  9. package/lib/__tests__/label-svg-icon.test.js +373 -0
  10. package/lib/__tests__/labels.test.js +61 -0
  11. package/lib/__tests__/mark-label.test.js +133 -0
  12. package/lib/__tests__/toggle-bar.test.js +197 -0
  13. package/lib/__tests__/tool-menu.test.js +151 -0
  14. package/lib/__tests__/undo-redo.test.js +31 -0
  15. package/lib/__tests__/use-debounce.test.js +24 -0
  16. package/lib/__tests__/utils.js +70 -0
  17. package/lib/__tests__/utils.test.js +123 -0
  18. package/lib/axis/__tests__/arrow.test.js +60 -0
  19. package/lib/axis/__tests__/axes.test.js +195 -0
  20. package/lib/container/__tests__/actions.test.js +108 -0
  21. package/lib/container/__tests__/index.test.js +503 -0
  22. package/lib/container/__tests__/marks.test.js +259 -0
  23. package/lib/container/__tests__/middleware.test.js +244 -0
  24. package/lib/container/__tests__/reducer.test.js +450 -0
  25. package/lib/mark-label.js +7 -197
  26. package/lib/mark-label.js.map +1 -1
  27. package/lib/tools/absolute/__tests__/component.test.js +65 -0
  28. package/lib/tools/circle/__tests__/bg-circle.test.js +33 -0
  29. package/lib/tools/circle/__tests__/component.test.js +615 -0
  30. package/lib/tools/circle/__tests__/index.test.js +576 -0
  31. package/lib/tools/circle/component.js +3 -34
  32. package/lib/tools/circle/component.js.map +1 -1
  33. package/lib/tools/exponential/__tests__/component.test.js +64 -0
  34. package/lib/tools/exponential/__tests__/index.test.js +828 -0
  35. package/lib/tools/line/__tests__/component.test.js +45 -0
  36. package/lib/tools/parabola/__tests__/component.test.js +64 -0
  37. package/lib/tools/parabola/__tests__/index.test.js +564 -0
  38. package/lib/tools/point/__tests__/component.test.js +434 -0
  39. package/lib/tools/point/__tests__/index.test.js +262 -0
  40. package/lib/tools/point/component.js +8 -29
  41. package/lib/tools/point/component.js.map +1 -1
  42. package/lib/tools/polygon/__tests__/component.test.js +485 -0
  43. package/lib/tools/polygon/__tests__/index.test.js +303 -0
  44. package/lib/tools/polygon/__tests__/line.test.js +50 -0
  45. package/lib/tools/polygon/__tests__/polygon.test.js +85 -0
  46. package/lib/tools/polygon/component.js +6 -31
  47. package/lib/tools/polygon/component.js.map +1 -1
  48. package/lib/tools/ray/__tests__/component.test.js +35 -0
  49. package/lib/tools/segment/__tests__/component.test.js +35 -0
  50. package/lib/tools/shared/__tests__/arrow-head.test.js +45 -0
  51. package/lib/tools/shared/line/__tests__/index.test.js +124 -0
  52. package/lib/tools/shared/line/__tests__/line-path.test.js +62 -0
  53. package/lib/tools/shared/line/__tests__/with-root-edge.test.js +90 -0
  54. package/lib/tools/shared/line/index.js +3 -34
  55. package/lib/tools/shared/line/index.js.map +1 -1
  56. package/lib/tools/shared/point/__tests__/arrow-point.test.js +127 -0
  57. package/lib/tools/shared/point/__tests__/base-point.test.js +122 -0
  58. package/lib/tools/sine/__tests__/component.test.js +72 -0
  59. package/lib/tools/vector/__tests__/component.test.js +32 -0
  60. package/package.json +17 -17
  61. package/src/__tests__/graph.test.jsx +1 -76
  62. package/src/__tests__/mark-label.test.jsx +1 -375
  63. package/src/mark-label.jsx +8 -209
  64. package/src/tools/circle/component.jsx +3 -26
  65. package/src/tools/point/__tests__/component.test.jsx +1 -127
  66. package/src/tools/point/component.jsx +8 -21
  67. package/src/tools/polygon/component.jsx +6 -24
  68. package/src/tools/shared/line/index.jsx +3 -26
  69. package/LICENSE.md +0 -5
@@ -1,4 +1,4 @@
1
- import { act, fireEvent, render } from '@pie-lib/test-utils';
1
+ import { render } from '@pie-lib/test-utils';
2
2
  import React from 'react';
3
3
  import { coordinates, MarkLabel, position } from '../mark-label';
4
4
  import { graphProps as getGraphProps } from './utils';
@@ -30,380 +30,6 @@ describe('MarkLabel', () => {
30
30
  });
31
31
  });
32
32
 
33
- describe('MarkLabel - editing', () => {
34
- let onChange;
35
- let onBlur;
36
-
37
- beforeEach(() => {
38
- onChange = jest.fn();
39
- onBlur = jest.fn();
40
- });
41
-
42
- // the label input is the only input MarkLabel renders (react-input-autosize)
43
- const input = (container) => container.querySelector('input');
44
-
45
- const renderComponent = (extras) => {
46
- const defaults = {
47
- onChange,
48
- onBlur,
49
- inputRef: jest.fn(),
50
- mark: { x: 1, y: 1, label: '' },
51
- graphProps: getGraphProps(0, 10, 0, 10),
52
- };
53
- const props = { ...defaults, ...extras };
54
- const result = render(<MarkLabel {...props} />);
55
-
56
- return {
57
- ...result,
58
- input: input(result.container),
59
- rerenderWith: (next) => result.rerender(<MarkLabel {...props} {...next} />),
60
- };
61
- };
62
-
63
- describe('autoFocus', () => {
64
- it('focuses the input so a new label can be typed into right away', () => {
65
- const { input } = renderComponent({ autoFocus: true });
66
-
67
- expect(document.activeElement).toBe(input);
68
- });
69
-
70
- it('leaves the caret at the end of an existing label', () => {
71
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'AB' } });
72
-
73
- expect(document.activeElement).toBe(input);
74
- expect(input.selectionStart).toBe(2);
75
- expect(input.selectionEnd).toBe(2);
76
- });
77
-
78
- it('does not focus the input of an existing label when autoFocus is not set', () => {
79
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: 'A' } });
80
-
81
- expect(document.activeElement).not.toBe(input);
82
- });
83
-
84
- it('does not focus the input when disabled', () => {
85
- const { input } = renderComponent({ autoFocus: true, disabled: true, mark: { x: 1, y: 1, label: 'A' } });
86
-
87
- expect(document.activeElement).not.toBe(input);
88
- });
89
-
90
- it('does not focus the input of a disabled mark', () => {
91
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'A', disabled: true } });
92
-
93
- expect(document.activeElement).not.toBe(input);
94
- });
95
-
96
- // the focus can go away without this component hearing about it - the input node gets replaced
97
- // by a render higher up, so no blur is fired and autoFocus is still on
98
- it('takes the focus back on the next render while it is still being edited', () => {
99
- const { input, rerenderWith } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'A' } });
100
-
101
- expect(document.activeElement).toBe(input);
102
-
103
- input.blur();
104
- expect(document.activeElement).not.toBe(input);
105
-
106
- rerenderWith({ mark: { x: 1, y: 1, label: 'A' } });
107
-
108
- expect(document.activeElement).toBe(input);
109
- });
110
-
111
- it('leaves the caret where the user put it while the input has the focus', () => {
112
- const { input, rerenderWith } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'ABC' } });
113
-
114
- input.setSelectionRange(1, 1);
115
-
116
- rerenderWith({ mark: { x: 1, y: 1, label: 'ABC' } });
117
-
118
- expect(document.activeElement).toBe(input);
119
- expect(input.selectionStart).toBe(1);
120
- });
121
-
122
- it('focuses the input once it is no longer disabled', () => {
123
- const { rerenderWith, container } = renderComponent({ autoFocus: true, disabled: true });
124
-
125
- rerenderWith({ disabled: false });
126
-
127
- expect(document.activeElement).toBe(input(container));
128
- });
129
-
130
- // the host saves the model with an api call and the model that comes back can replace the
131
- // input node - the label has to keep the focus across that remount
132
- it('re-focuses the input when it is remounted while being edited', () => {
133
- const props = {
134
- autoFocus: true,
135
- onChange,
136
- inputRef: jest.fn(),
137
- mark: { x: 1, y: 1, label: 'A' },
138
- graphProps: getGraphProps(0, 10, 0, 10),
139
- };
140
- const { container, rerender } = render(<MarkLabel key="first" {...props} />);
141
- const first = input(container);
142
-
143
- expect(document.activeElement).toBe(first);
144
-
145
- rerender(<MarkLabel key="second" {...props} />);
146
- const second = input(container);
147
-
148
- expect(second).not.toBe(first);
149
- expect(document.activeElement).toBe(second);
150
- });
151
- });
152
-
153
- // the tool asks for the focus with autoFocus, but its state can be gone before the input renders:
154
- // the marks come back from the host asynchronously and can remount the tool. An empty label in
155
- // label mode is a label the user has just added, so the input can tell on its own.
156
- describe('a label that was just added', () => {
157
- it('focuses itself without the tool asking', () => {
158
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
159
-
160
- expect(document.activeElement).toBe(input);
161
- });
162
-
163
- it('focuses itself again when it is remounted before it was typed into', () => {
164
- const props = {
165
- onChange,
166
- onBlur,
167
- inputRef: jest.fn(),
168
- mark: { x: 1, y: 1, label: '' },
169
- graphProps: getGraphProps(0, 10, 0, 10),
170
- };
171
- const { container, rerender } = render(<MarkLabel key="first" {...props} />);
172
- const first = input(container);
173
-
174
- rerender(<MarkLabel key="second" {...props} />);
175
- const second = input(container);
176
-
177
- expect(second).not.toBe(first);
178
- expect(document.activeElement).toBe(second);
179
- });
180
-
181
- it('stops chasing the focus once it has been left', () => {
182
- const { input, rerenderWith } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
183
-
184
- expect(document.activeElement).toBe(input);
185
-
186
- input.blur();
187
- rerenderWith({ mark: { x: 1, y: 1, label: '' } });
188
-
189
- expect(document.activeElement).not.toBe(input);
190
- });
191
-
192
- // outside of label mode the input is disabled, which is how an empty label that was loaded with
193
- // the item is told apart from one the user has just added - label mode always starts off
194
- // the config draws the background marks into the correct answer graphs as disabled marks
195
- it('is not rendered at all while it cannot be edited', () => {
196
- const { container } = renderComponent({ disabled: true, mark: { x: 1, y: 1, label: '' } });
197
-
198
- expect(container.querySelector('input')).toBe(null);
199
- });
200
-
201
- it('is still rendered uneditable once it has text', () => {
202
- const { container } = renderComponent({ disabled: true, mark: { x: 1, y: 1, label: 'A' } });
203
-
204
- expect(container.querySelector('input').value).toBe('A');
205
- });
206
-
207
- it('is not focused when label mode is turned on afterwards', () => {
208
- const { rerenderWith, container } = renderComponent({ disabled: true, mark: { x: 1, y: 1, label: '' } });
209
-
210
- rerenderWith({ disabled: false });
211
-
212
- expect(input(container)).not.toBe(null);
213
- expect(document.activeElement).not.toBe(input(container));
214
- });
215
-
216
- it('is not rendered for a disabled mark while it is empty', () => {
217
- const { container } = renderComponent({ mark: { x: 1, y: 1, label: '', disabled: true } });
218
-
219
- expect(container.querySelector('input')).toBe(null);
220
- });
221
-
222
- // an input sized to its content is 2px wide while it is empty, and chrome paints the caret of
223
- // an input inside an svg foreignObject as a hairline that never blinks - so an empty label that
224
- // has the focus has to be wide enough to be seen as a box waiting for text
225
- it('is wide enough to be seen while it is empty', () => {
226
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
227
-
228
- expect(parseInt(input.style.width, 10)).toBeGreaterThanOrEqual(30);
229
- });
230
-
231
- it('is only as wide as its text once it cannot be edited', () => {
232
- const { container } = renderComponent({ disabled: true, mark: { x: 1, y: 1, label: 'A' } });
233
-
234
- expect(parseInt(container.querySelector('input').style.width, 10)).toBeLessThan(30);
235
- });
236
- });
237
-
238
- describe('syncing mark.label into the input', () => {
239
- it('picks up a new mark.label while the input does not have the focus', () => {
240
- const { rerenderWith, container } = renderComponent({ mark: { x: 1, y: 1, label: 'A' } });
241
-
242
- rerenderWith({ mark: { x: 1, y: 1, label: 'updated' } });
243
-
244
- expect(input(container).value).toBe('updated');
245
- });
246
-
247
- // a mark coming back from an (async) model save can carry an older label - applying it would
248
- // wipe out the characters typed in the meantime
249
- it('keeps what the user typed while the input has the focus', () => {
250
- const { input, rerenderWith } = renderComponent({ autoFocus: true });
251
-
252
- expect(document.activeElement).toBe(input);
253
-
254
- fireEvent.change(input, { target: { value: 'AB' } });
255
- expect(input.value).toBe('AB');
256
-
257
- // the host echoes back the label as it was one keystroke ago
258
- rerenderWith({ mark: { x: 1, y: 1, label: 'A' } });
259
-
260
- expect(input.value).toBe('AB');
261
- });
262
- });
263
-
264
- // the characters land at the document's caret, not at the focused node. In the authoring
265
- // environment the input ends up focused with no caret anywhere: keypress fires and nothing is
266
- // inserted. jsdom never reports a caret either, so this is that state - the focus cycle that
267
- // repairs it must not hand the label over to the blur handling.
268
- describe('a focused input with no caret', () => {
269
- it('is repaired without reporting the label as removed', () => {
270
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: '' } });
271
-
272
- expect(document.activeElement).toBe(input);
273
- expect(onChange).not.toHaveBeenCalled();
274
- expect(onBlur).not.toHaveBeenCalled();
275
- });
276
-
277
- it('leaves the caret at the end of a label that already has text', () => {
278
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'AB' } });
279
-
280
- expect(document.activeElement).toBe(input);
281
- expect(input.selectionStart).toBe(2);
282
- expect(onChange).not.toHaveBeenCalled();
283
- });
284
- });
285
-
286
- // clicking another window, the devtools, or the host moving the focus while it saves the model:
287
- // the label is not being left, so it must survive and take the focus back
288
- describe('while the window is away', () => {
289
- const windowGoesAway = () => window.dispatchEvent(new Event('blur'));
290
- const windowComesBack = () => window.dispatchEvent(new Event('focus'));
291
-
292
- it('keeps an empty label instead of reporting it as removed', () => {
293
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
294
-
295
- windowGoesAway();
296
- fireEvent.blur(input);
297
-
298
- expect(onChange).not.toHaveBeenCalled();
299
- expect(onBlur).not.toHaveBeenCalled();
300
- });
301
-
302
- it('takes the focus back when the window comes back', () => {
303
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: '' } });
304
-
305
- // the focus is moved to another input rather than with input.blur(): jsdom fires a window
306
- // focus event as part of blur(), which is the very thing being tested here
307
- const elsewhere = document.createElement('input');
308
-
309
- document.body.appendChild(elsewhere);
310
-
311
- windowGoesAway();
312
- elsewhere.focus();
313
- expect(document.activeElement).toBe(elsewhere);
314
-
315
- windowComesBack();
316
-
317
- expect(document.activeElement).toBe(input);
318
-
319
- elsewhere.remove();
320
- });
321
-
322
- it('still reports a label the user leaves once the window is back', () => {
323
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
324
-
325
- windowGoesAway();
326
- windowComesBack();
327
- fireEvent.blur(input);
328
-
329
- expect(onChange).toHaveBeenCalledWith('');
330
- });
331
- });
332
-
333
- describe('onBlur', () => {
334
- it('is called so the tool stops auto focusing the label', () => {
335
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'A' } });
336
-
337
- fireEvent.blur(input);
338
-
339
- expect(onBlur).toHaveBeenCalled();
340
- });
341
-
342
- // clicking a button or anything else interactive blurs the label while the debounce is pending
343
- it('saves what was typed straight away', () => {
344
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
345
-
346
- fireEvent.change(input, { target: { value: 'AB' } });
347
- fireEvent.blur(input);
348
-
349
- expect(onChange).toHaveBeenCalledWith('AB');
350
- });
351
-
352
- it('does not save it twice when the debounce catches up', () => {
353
- jest.useFakeTimers();
354
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '' } });
355
-
356
- fireEvent.change(input, { target: { value: 'AB' } });
357
- fireEvent.blur(input);
358
- act(() => jest.advanceTimersByTime(1000));
359
-
360
- expect(onChange).toHaveBeenCalledTimes(1);
361
- expect(onChange).toHaveBeenCalledWith('AB');
362
- jest.useRealTimers();
363
- });
364
-
365
- it('reports an empty label on blur', () => {
366
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: 'A' } });
367
-
368
- fireEvent.change(input, { target: { value: '' } });
369
- fireEvent.blur(input);
370
-
371
- expect(onChange).toHaveBeenCalledWith('');
372
- });
373
- });
374
-
375
- describe('debounce', () => {
376
- beforeEach(() => jest.useFakeTimers());
377
- afterEach(() => jest.useRealTimers());
378
-
379
- it('does not report the typed label before the debounce delay elapses', () => {
380
- const { input } = renderComponent({ autoFocus: true });
381
-
382
- fireEvent.change(input, { target: { value: 'A' } });
383
-
384
- act(() => jest.advanceTimersByTime(499));
385
- expect(onChange).not.toHaveBeenCalled();
386
-
387
- act(() => jest.advanceTimersByTime(1));
388
- expect(onChange).toHaveBeenCalledWith('A');
389
- });
390
-
391
- it('reports the label once for a burst of keystrokes', () => {
392
- const { input } = renderComponent({ autoFocus: true });
393
-
394
- fireEvent.change(input, { target: { value: 'A' } });
395
- act(() => jest.advanceTimersByTime(300));
396
- fireEvent.change(input, { target: { value: 'AB' } });
397
- act(() => jest.advanceTimersByTime(300));
398
- fireEvent.change(input, { target: { value: 'ABC' } });
399
- act(() => jest.advanceTimersByTime(500));
400
-
401
- expect(onChange).toHaveBeenCalledTimes(1);
402
- expect(onChange).toHaveBeenCalledWith('ABC');
403
- });
404
- });
405
- });
406
-
407
33
  describe('position', () => {
408
34
  const assertPosition = (mark, rect, expected) => {
409
35
  it(`${mark.x},${mark.y} + ${rect.width},${rect.height} => ${expected}`, () => {
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled, useTheme } from '@mui/material/styles';
4
4
  import AutosizeInput from 'react-input-autosize';
@@ -7,18 +7,6 @@ import { types } from '@pie-lib/plot';
7
7
  import { color } from '@pie-lib/render-ui';
8
8
  import SvgIcon from './label-svg-icon';
9
9
 
10
- const DEBOUNCE_DELAY = 500;
11
- // A new label insists on the focus and the caret for this long, and again on each of the ticks
12
- // below. No single moment is reliable: the authoring host re-renders the whole element while it
13
- // saves, and the input is replaced under this component more than once. Nothing typed yet is what
14
- // tells a transient blur apart from the user leaving the label.
15
- const INSIST_ON_FOCUS_FOR = 500;
16
- const INSIST_AT = [0, 50, 120, 250, 400, 700, 1000];
17
- // how many times a label will do the heavier focus cycle to get a caret back
18
- const MAX_CARET_CYCLES = 3;
19
- // how wide an editable label input is while it is empty
20
- const EMPTY_INPUT_WIDTH = 30;
21
-
22
10
  const StyledInputCorrect = styled('div')(({ theme }) => ({
23
11
  float: 'right',
24
12
  padding: theme.spacing(0.5),
@@ -115,28 +103,7 @@ export const coordinates = (graphProps, mark, rect = { width: 0, height: 0 }, po
115
103
  }
116
104
  };
117
105
 
118
- /**
119
- * Whether the document's caret is in this input. The characters land at the caret, not at the
120
- * focused node - an input can be the active element with no caret anywhere, and then keypress fires
121
- * and nothing is inserted, with nothing in the page cancelling anything.
122
- */
123
- const caretIsInside = (node) => {
124
- if (typeof document === 'undefined' || !document.getSelection) {
125
- return true;
126
- }
127
-
128
- const selection = document.getSelection();
129
-
130
- if (!selection || !selection.rangeCount || !selection.anchorNode) {
131
- return false;
132
- }
133
-
134
- const anchor = selection.anchorNode;
135
-
136
- return anchor === node || anchor === node.parentNode || node.contains(anchor);
137
- };
138
-
139
- const LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, minWidth, onChange, onBlur }) => (
106
+ const LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, onChange, onBlur }) => (
140
107
  <AutosizeInput
141
108
  inputRef={(r) => {
142
109
  _ref(r);
@@ -144,7 +111,6 @@ const LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, minWi
144
111
  }}
145
112
  disabled={disabled}
146
113
  inputStyle={inputStyle}
147
- minWidth={minWidth}
148
114
  value={label}
149
115
  onChange={onChange}
150
116
  onBlur={onBlur}
@@ -157,196 +123,42 @@ LabelInput.propTypes = {
157
123
  label: PropTypes.string,
158
124
  disabled: PropTypes.bool,
159
125
  inputStyle: PropTypes.object,
160
- minWidth: PropTypes.number,
161
126
  onChange: PropTypes.func,
162
127
  onBlur: PropTypes.func,
163
128
  };
164
129
 
165
130
  export const MarkLabel = (props) => {
166
- // the state repositions the label once the input has a size, the ref is never a render behind
167
- const inputNode = useRef(null);
168
131
  const [input, setInput] = useState(null);
169
- const _ref = useCallback((node) => {
170
- inputNode.current = node;
171
- setInput(node);
172
- }, []);
132
+ const _ref = useCallback((node) => setInput(node));
173
133
  const theme = useTheme();
174
134
 
175
- const { mark, graphProps, disabled, autoFocus, inputRef: externalInputRef } = props;
135
+ const { mark, graphProps, disabled, inputRef: externalInputRef } = props;
176
136
 
177
137
  const [label, setLabel] = useState(mark.label);
178
138
  const { correctness, correctnesslabel, correctlabel } = mark;
179
139
 
180
- const isFocused = () =>
181
- !!inputNode.current && typeof document !== 'undefined' && document.activeElement === inputNode.current;
182
-
183
- // an empty label with an enabled input is one the user just added: label mode has to be on and it
184
- // always starts off. Read from the mark, not from autoFocus, because the tool that asked for the
185
- // label can be remounted by the marks coming back from the host before the input renders.
186
- const isNewLabel = useRef(mark.label === '' && !disabled && !mark.disabled);
187
- const insistUntil = useRef(0);
188
- const lastSaved = useRef(mark.label);
189
- const insisting = () => Date.now() < insistUntil.current && !(inputNode.current && inputNode.current.value);
190
- // the whole window losing the focus is not the user leaving the label - another window, the
191
- // devtools, or the host moving the focus while it saves. The window blur arrives before the
192
- // input's own blur, so this flag is set by the time the label is asked to give itself up.
193
- const windowAway = useRef(false);
194
- // our own blur, from the focus cycle that puts a missing caret back
195
- const selfFocusing = useRef(false);
196
- const caretCycles = useRef(0);
197
-
198
140
  const onChange = (e) => setLabel(e.target.value);
199
141
 
200
- const saveLabel = (value) => {
201
- if (value === lastSaved.current) {
202
- return;
203
- }
204
-
205
- lastSaved.current = value;
206
- props.onChange(value);
207
- };
208
-
209
142
  const handleBlur = useCallback(() => {
210
- // in the DNA env the focus is moved while the model is saved - not the user leaving the label
211
- if (insisting() || windowAway.current || selfFocusing.current) {
212
- return;
213
- }
214
-
215
- isNewLabel.current = false;
216
-
217
143
  if (label === '') {
218
144
  props.onChange('');
219
- } else if (label !== mark.label) {
220
- // the debounce can still be pending, and once the focus is gone an echo of the model would
221
- // overwrite what was typed before it fires
222
- saveLabel(label);
223
- }
224
-
225
- // lets the tool know that this label is not being edited anymore, so it stops auto focusing it
226
- if (props.onBlur) {
227
- props.onBlur();
228
145
  }
229
- }, [label, mark.label, props.onChange, props.onBlur]);
146
+ }, [label, props.onChange]);
230
147
 
231
- const debouncedLabel = useDebounce(label, DEBOUNCE_DELAY);
148
+ const debouncedLabel = useDebounce(label, 200);
232
149
 
233
- // props to state, but not while focused: the mark can come back from an async model save with an
234
- // older label and overwrite what was typed meanwhile
150
+ // useState only sets the value once, to synch props to state need useEffect
235
151
  useEffect(() => {
236
- if (isFocused()) {
237
- return;
238
- }
239
-
240
152
  setLabel(mark.label);
241
153
  }, [mark.label]);
242
154
 
243
155
  // pick up the change to debouncedLabel and save it
244
156
  useEffect(() => {
245
157
  if (typeof debouncedLabel === 'string' && debouncedLabel !== mark.label) {
246
- saveLabel(debouncedLabel);
158
+ props.onChange(debouncedLabel);
247
159
  }
248
160
  }, [debouncedLabel]);
249
161
 
250
- const focusInput = () => {
251
- const node = inputNode.current;
252
-
253
- // node.disabled, not the disabled prop: a disabled input silently ignores focus()
254
- if ((!autoFocus && !isNewLabel.current && !insisting()) || !node || node.disabled) {
255
- return;
256
- }
257
-
258
- const wasFocused = isFocused();
259
-
260
- if (!wasFocused) {
261
- node.focus();
262
-
263
- // a new label is focused once, so it cannot fight over the focus with another one
264
- if (isFocused()) {
265
- isNewLabel.current = false;
266
- }
267
- }
268
-
269
- if (!node.setSelectionRange) {
270
- return;
271
- }
272
-
273
- // a label that was already being edited keeps the caret the user put in it; one that has just
274
- // taken the focus gets it at the end, otherwise typing continues in front of the existing label
275
- const keepCaret = wasFocused && node.value;
276
- const start = keepCaret ? node.selectionStart : (node.value || '').length;
277
- const end = keepCaret ? node.selectionEnd : start;
278
-
279
- // The characters land at the document's caret, not at the focused node. An input focused while
280
- // it was being replaced, or while the window was away, ends up as the active element with no
281
- // caret anywhere: keypress fires and nothing is inserted, with nothing in the page cancelling
282
- // anything. Re-asserting the range is what puts the caret back.
283
- if (!caretIsInside(node)) {
284
- node.setSelectionRange(start, end);
285
- }
286
-
287
- // and if it still is not there, a focus cycle is the heavier way to get it. Every focusInput
288
- // call is another go at it - after each render, on each insist tick, and when the window comes
289
- // back - up to a few times per label, since the state comes back as the input is replaced.
290
- if (caretIsInside(node) || caretCycles.current >= MAX_CARET_CYCLES) {
291
- return;
292
- }
293
-
294
- caretCycles.current += 1;
295
- // our own blur, so the label is not given up over it
296
- selfFocusing.current = true;
297
- node.blur();
298
- node.focus();
299
- node.setSelectionRange(start, end);
300
- selfFocusing.current = false;
301
- };
302
-
303
- // after every render: the input can appear a render or more later (the mark comes back from an
304
- // async model save) and a replaced node loses the focus without this component being told
305
- useEffect(() => focusInput());
306
-
307
- // and again on a timer, for the DNA env, where the focus is moved once the renders are over. The
308
- // original code focused from a setTimeout, which is what let it survive that.
309
- useEffect(() => {
310
- if (!autoFocus && !isNewLabel.current) {
311
- return;
312
- }
313
-
314
- if (!insistUntil.current) {
315
- insistUntil.current = Date.now() + INSIST_ON_FOCUS_FOR;
316
- }
317
-
318
- const timers = INSIST_AT.map((delay) => setTimeout(focusInput, delay));
319
-
320
- return () => timers.forEach((t) => clearTimeout(t));
321
- }, [autoFocus]);
322
-
323
- // the blur that came with the window going away is ignored, so the label is still the one being
324
- // edited when the window comes back and it takes the focus, and its selection, back
325
- useEffect(() => {
326
- const onWindowBlur = () => {
327
- windowAway.current = true;
328
- };
329
-
330
- const onWindowFocus = () => {
331
- // coming back only means something if the window went away: moving the focus from one element
332
- // to another inside the page reaches window too in some implementations
333
- if (!windowAway.current) {
334
- return;
335
- }
336
-
337
- windowAway.current = false;
338
- focusInput();
339
- };
340
-
341
- window.addEventListener('blur', onWindowBlur);
342
- window.addEventListener('focus', onWindowFocus);
343
-
344
- return () => {
345
- window.removeEventListener('blur', onWindowBlur);
346
- window.removeEventListener('focus', onWindowFocus);
347
- };
348
- }, [autoFocus]);
349
-
350
162
  const rect = input ? input.getBoundingClientRect() : { width: 0, height: 0 };
351
163
  const pos = position(graphProps, mark, rect);
352
164
  const leftTop = coordinates(graphProps, mark, rect, pos);
@@ -374,11 +186,6 @@ export const MarkLabel = (props) => {
374
186
  label={labelValue}
375
187
  disabled={disabledInput}
376
188
  inputStyle={inputStyle}
377
- // an editable label sizes itself to its content, so an empty one is 2px wide: enough for the
378
- // caret, not enough for anybody to see that the label is waiting to be typed into. Chrome
379
- // does not blink the caret of an input inside an svg foreignObject, it only paints a hairline,
380
- // so the box itself has to say that it has the focus.
381
- minWidth={disabledInput ? 1 : EMPTY_INPUT_WIDTH}
382
189
  onChange={onChange}
383
190
  onBlur={handleBlur}
384
191
  />
@@ -423,19 +230,11 @@ export const MarkLabel = (props) => {
423
230
  return <StyledIncorrect style={style}>{renderInput(studentInputStyle, label)}</StyledIncorrect>;
424
231
  }
425
232
 
426
- // an empty label that cannot be edited is just an empty box - the config draws the background
427
- // marks into every correct answer graph too
428
- if (disabledInput && !label) {
429
- return null;
430
- }
431
-
432
233
  return <div style={style}>{renderInput(getInputStyles(theme, disabled, mark.disabled), label)}</div>;
433
234
  };
434
235
 
435
236
  MarkLabel.propTypes = {
436
- autoFocus: PropTypes.bool,
437
237
  disabled: PropTypes.bool,
438
- onBlur: PropTypes.func,
439
238
  onChange: PropTypes.func,
440
239
  graphProps: types.GraphPropsType,
441
240
  inputRef: PropTypes.func,