@pie-lib/graphing 4.0.17 → 4.0.19

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 CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [4.0.19](https://github.com/pie-framework/pie-lib/compare/@pie-lib/graphing@4.0.18...@pie-lib/graphing@4.0.19) (2026-08-28)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **graphing:** give a new label a caret, not just the focus PIE-681 ([eccf0df](https://github.com/pie-framework/pie-lib/commit/eccf0df50183eab617bf492eec197a1ce9a55087))
11
+
12
+ ## [4.0.18](https://github.com/pie-framework/pie-lib/compare/@pie-lib/graphing@4.0.17...@pie-lib/graphing@4.0.18) (2026-08-26)
13
+
14
+ ### Bug Fixes
15
+
16
+ - **graphing:** hold the focus on a new label and save it on blur PIE-681 ([26c5f98](https://github.com/pie-framework/pie-lib/commit/26c5f9875b45f4495d76827633f28d82d93d8cb9))
17
+
6
18
  ## [4.0.17](https://github.com/pie-framework/pie-lib/compare/@pie-lib/graphing@4.0.16...@pie-lib/graphing@4.0.17) (2026-08-21)
7
19
 
8
20
  ### Bug Fixes
package/lib/mark-label.js CHANGED
@@ -20,6 +20,16 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
20
20
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
21
21
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
22
22
  var DEBOUNCE_DELAY = 500;
23
+ // A new label insists on the focus and the caret for this long, and again on each of the ticks
24
+ // below. No single moment is reliable: the authoring host re-renders the whole element while it
25
+ // saves, and the input is replaced under this component more than once. Nothing typed yet is what
26
+ // tells a transient blur apart from the user leaving the label.
27
+ var INSIST_ON_FOCUS_FOR = 500;
28
+ var INSIST_AT = [0, 50, 120, 250, 400, 700, 1000];
29
+ // how many times a label will do the heavier focus cycle to get a caret back
30
+ var MAX_CARET_CYCLES = 3;
31
+ // how wide an editable label input is while it is empty
32
+ var EMPTY_INPUT_WIDTH = 30;
23
33
  var StyledInputCorrect = (0, _styles.styled)('div')(function (_ref2) {
24
34
  var theme = _ref2.theme;
25
35
  return {
@@ -138,12 +148,30 @@ var coordinates = exports.coordinates = function coordinates(graphProps, mark) {
138
148
  return {};
139
149
  }
140
150
  };
151
+
152
+ /**
153
+ * Whether the document's caret is in this input. The characters land at the caret, not at the
154
+ * focused node - an input can be the active element with no caret anywhere, and then keypress fires
155
+ * and nothing is inserted, with nothing in the page cancelling anything.
156
+ */
157
+ var caretIsInside = function caretIsInside(node) {
158
+ if (typeof document === 'undefined' || !document.getSelection) {
159
+ return true;
160
+ }
161
+ var selection = document.getSelection();
162
+ if (!selection || !selection.rangeCount || !selection.anchorNode) {
163
+ return false;
164
+ }
165
+ var anchor = selection.anchorNode;
166
+ return anchor === node || anchor === node.parentNode || node.contains(anchor);
167
+ };
141
168
  var LabelInput = function LabelInput(_ref5) {
142
169
  var _ref = _ref5._ref,
143
170
  externalInputRef = _ref5.externalInputRef,
144
171
  label = _ref5.label,
145
172
  disabled = _ref5.disabled,
146
173
  inputStyle = _ref5.inputStyle,
174
+ minWidth = _ref5.minWidth,
147
175
  onChange = _ref5.onChange,
148
176
  onBlur = _ref5.onBlur;
149
177
  return /*#__PURE__*/_react["default"].createElement(_reactInputAutosize["default"], {
@@ -153,6 +181,7 @@ var LabelInput = function LabelInput(_ref5) {
153
181
  },
154
182
  disabled: disabled,
155
183
  inputStyle: inputStyle,
184
+ minWidth: minWidth,
156
185
  value: label,
157
186
  onChange: onChange,
158
187
  onBlur: onBlur
@@ -164,12 +193,12 @@ LabelInput.propTypes = {
164
193
  label: _propTypes["default"].string,
165
194
  disabled: _propTypes["default"].bool,
166
195
  inputStyle: _propTypes["default"].object,
196
+ minWidth: _propTypes["default"].number,
167
197
  onChange: _propTypes["default"].func,
168
198
  onBlur: _propTypes["default"].func
169
199
  };
170
200
  var MarkLabel = exports.MarkLabel = function MarkLabel(props) {
171
- // the node is kept in a ref as well as in the state: the state is needed to reposition the label
172
- // once it has a size, the ref is what the focus is asserted on - it is never a render behind
201
+ // the state repositions the label once the input has a size, the ref is never a render behind
173
202
  var inputNode = (0, _react.useRef)(null);
174
203
  var _useState = (0, _react.useState)(null),
175
204
  _useState2 = (0, _slicedToArray2["default"])(_useState, 2),
@@ -196,31 +225,55 @@ var MarkLabel = exports.MarkLabel = function MarkLabel(props) {
196
225
  return !!inputNode.current && typeof document !== 'undefined' && document.activeElement === inputNode.current;
197
226
  };
198
227
 
199
- // A label that has just been added is empty and its input is enabled - label mode has to be on
200
- // for that to happen and it always starts off, so a mark that was loaded with an empty label
201
- // cannot grab the focus. This is read from the mark rather than taken from the autoFocus prop
202
- // because the tool that asked for the label can be remounted by the marks coming back from the
203
- // host (async model save) before the input ever renders, and its autoFocus goes with it.
228
+ // an empty label with an enabled input is one the user just added: label mode has to be on and it
229
+ // always starts off. Read from the mark, not from autoFocus, because the tool that asked for the
230
+ // label can be remounted by the marks coming back from the host before the input renders.
204
231
  var isNewLabel = (0, _react.useRef)(mark.label === '' && !disabled && !mark.disabled);
232
+ var insistUntil = (0, _react.useRef)(0);
233
+ var lastSaved = (0, _react.useRef)(mark.label);
234
+ var insisting = function insisting() {
235
+ return Date.now() < insistUntil.current && !(inputNode.current && inputNode.current.value);
236
+ };
237
+ // the whole window losing the focus is not the user leaving the label - another window, the
238
+ // devtools, or the host moving the focus while it saves. The window blur arrives before the
239
+ // input's own blur, so this flag is set by the time the label is asked to give itself up.
240
+ var windowAway = (0, _react.useRef)(false);
241
+ // our own blur, from the focus cycle that puts a missing caret back
242
+ var selfFocusing = (0, _react.useRef)(false);
243
+ var caretCycles = (0, _react.useRef)(0);
205
244
  var onChange = function onChange(e) {
206
245
  return setLabel(e.target.value);
207
246
  };
247
+ var saveLabel = function saveLabel(value) {
248
+ if (value === lastSaved.current) {
249
+ return;
250
+ }
251
+ lastSaved.current = value;
252
+ props.onChange(value);
253
+ };
208
254
  var handleBlur = (0, _react.useCallback)(function () {
255
+ // in the DNA env the focus is moved while the model is saved - not the user leaving the label
256
+ if (insisting() || windowAway.current || selfFocusing.current) {
257
+ return;
258
+ }
209
259
  isNewLabel.current = false;
210
260
  if (label === '') {
211
261
  props.onChange('');
262
+ } else if (label !== mark.label) {
263
+ // the debounce can still be pending, and once the focus is gone an echo of the model would
264
+ // overwrite what was typed before it fires
265
+ saveLabel(label);
212
266
  }
213
267
 
214
268
  // lets the tool know that this label is not being edited anymore, so it stops auto focusing it
215
269
  if (props.onBlur) {
216
270
  props.onBlur();
217
271
  }
218
- }, [label, props.onChange, props.onBlur]);
272
+ }, [label, mark.label, props.onChange, props.onBlur]);
219
273
  var debouncedLabel = (0, _useDebounce.useDebounce)(label, DEBOUNCE_DELAY);
220
274
 
221
- // useState only sets the value once, to synch props to state need useEffect.
222
- // While this input has the focus we keep what the user typed: the mark can come back from an
223
- // (async) model save with an older label and that would overwrite the characters typed meanwhile.
275
+ // props to state, but not while focused: the mark can come back from an async model save with an
276
+ // older label and overwrite what was typed meanwhile
224
277
  (0, _react.useEffect)(function () {
225
278
  if (isFocused()) {
226
279
  return;
@@ -231,36 +284,105 @@ var MarkLabel = exports.MarkLabel = function MarkLabel(props) {
231
284
  // pick up the change to debouncedLabel and save it
232
285
  (0, _react.useEffect)(function () {
233
286
  if (typeof debouncedLabel === 'string' && debouncedLabel !== mark.label) {
234
- props.onChange(debouncedLabel);
287
+ saveLabel(debouncedLabel);
235
288
  }
236
289
  }, [debouncedLabel]);
237
-
238
- // A label that has just been added has to be focused so that it can be typed into right away.
239
- // This runs after every render on purpose - there is no reliable single moment to focus at:
240
- // the input shows up whenever the mark comes back from the (async) model save, and a remount
241
- // of the input (same save, replaced node) drops the focus without this component being told.
242
- // Asserting the focus on every render is what makes it stick; it is a no-op once the input has
243
- // it, so the caret the user placed is left alone.
244
- (0, _react.useEffect)(function () {
290
+ var focusInput = function focusInput() {
245
291
  var node = inputNode.current;
246
292
 
247
293
  // node.disabled, not the disabled prop: a disabled input silently ignores focus()
248
- if (!autoFocus && !isNewLabel.current || !node || node.disabled || isFocused()) {
294
+ if (!autoFocus && !isNewLabel.current && !insisting() || !node || node.disabled) {
249
295
  return;
250
296
  }
251
- node.focus();
297
+ var wasFocused = isFocused();
298
+ if (!wasFocused) {
299
+ node.focus();
252
300
 
253
- // a new label is focused once, so it cannot fight over the focus with another one
254
- if (isFocused()) {
255
- isNewLabel.current = false;
301
+ // a new label is focused once, so it cannot fight over the focus with another one
302
+ if (isFocused()) {
303
+ isNewLabel.current = false;
304
+ }
305
+ }
306
+ if (!node.setSelectionRange) {
307
+ return;
256
308
  }
257
309
 
258
- // keep the caret at the end, otherwise typing continues in front of the existing label
259
- var caret = (node.value || '').length;
260
- if (node.setSelectionRange) {
261
- node.setSelectionRange(caret, caret);
310
+ // a label that was already being edited keeps the caret the user put in it; one that has just
311
+ // taken the focus gets it at the end, otherwise typing continues in front of the existing label
312
+ var keepCaret = wasFocused && node.value;
313
+ var start = keepCaret ? node.selectionStart : (node.value || '').length;
314
+ var end = keepCaret ? node.selectionEnd : start;
315
+
316
+ // The characters land at the document's caret, not at the focused node. An input focused while
317
+ // it was being replaced, or while the window was away, ends up as the active element with no
318
+ // caret anywhere: keypress fires and nothing is inserted, with nothing in the page cancelling
319
+ // anything. Re-asserting the range is what puts the caret back.
320
+ if (!caretIsInside(node)) {
321
+ node.setSelectionRange(start, end);
262
322
  }
323
+
324
+ // and if it still is not there, a focus cycle is the heavier way to get it. Every focusInput
325
+ // call is another go at it - after each render, on each insist tick, and when the window comes
326
+ // back - up to a few times per label, since the state comes back as the input is replaced.
327
+ if (caretIsInside(node) || caretCycles.current >= MAX_CARET_CYCLES) {
328
+ return;
329
+ }
330
+ caretCycles.current += 1;
331
+ // our own blur, so the label is not given up over it
332
+ selfFocusing.current = true;
333
+ node.blur();
334
+ node.focus();
335
+ node.setSelectionRange(start, end);
336
+ selfFocusing.current = false;
337
+ };
338
+
339
+ // after every render: the input can appear a render or more later (the mark comes back from an
340
+ // async model save) and a replaced node loses the focus without this component being told
341
+ (0, _react.useEffect)(function () {
342
+ return focusInput();
263
343
  });
344
+
345
+ // and again on a timer, for the DNA env, where the focus is moved once the renders are over. The
346
+ // original code focused from a setTimeout, which is what let it survive that.
347
+ (0, _react.useEffect)(function () {
348
+ if (!autoFocus && !isNewLabel.current) {
349
+ return;
350
+ }
351
+ if (!insistUntil.current) {
352
+ insistUntil.current = Date.now() + INSIST_ON_FOCUS_FOR;
353
+ }
354
+ var timers = INSIST_AT.map(function (delay) {
355
+ return setTimeout(focusInput, delay);
356
+ });
357
+ return function () {
358
+ return timers.forEach(function (t) {
359
+ return clearTimeout(t);
360
+ });
361
+ };
362
+ }, [autoFocus]);
363
+
364
+ // the blur that came with the window going away is ignored, so the label is still the one being
365
+ // edited when the window comes back and it takes the focus, and its selection, back
366
+ (0, _react.useEffect)(function () {
367
+ var onWindowBlur = function onWindowBlur() {
368
+ windowAway.current = true;
369
+ };
370
+ var onWindowFocus = function onWindowFocus() {
371
+ // coming back only means something if the window went away: moving the focus from one element
372
+ // to another inside the page reaches window too in some implementations
373
+ if (!windowAway.current) {
374
+ return;
375
+ }
376
+ windowAway.current = false;
377
+ focusInput();
378
+ };
379
+ window.addEventListener('blur', onWindowBlur);
380
+ window.addEventListener('focus', onWindowFocus);
381
+ return function () {
382
+ window.removeEventListener('blur', onWindowBlur);
383
+ window.removeEventListener('focus', onWindowFocus);
384
+ };
385
+ }, [autoFocus]);
264
386
  var rect = input ? input.getBoundingClientRect() : {
265
387
  width: 0,
266
388
  height: 0
@@ -284,7 +406,13 @@ var MarkLabel = exports.MarkLabel = function MarkLabel(props) {
284
406
  externalInputRef: externalInputRef,
285
407
  label: labelValue,
286
408
  disabled: disabledInput,
287
- inputStyle: inputStyle,
409
+ inputStyle: inputStyle
410
+ // an editable label sizes itself to its content, so an empty one is 2px wide: enough for the
411
+ // caret, not enough for anybody to see that the label is waiting to be typed into. Chrome
412
+ // does not blink the caret of an input inside an svg foreignObject, it only paints a hairline,
413
+ // so the box itself has to say that it has the focus.
414
+ ,
415
+ minWidth: disabledInput ? 1 : EMPTY_INPUT_WIDTH,
288
416
  onChange: onChange,
289
417
  onBlur: handleBlur
290
418
  });
@@ -324,6 +452,12 @@ var MarkLabel = exports.MarkLabel = function MarkLabel(props) {
324
452
  style: style
325
453
  }, renderInput(studentInputStyle, label));
326
454
  }
455
+
456
+ // an empty label that cannot be edited is just an empty box - the config draws the background
457
+ // marks into every correct answer graph too
458
+ if (disabledInput && !label) {
459
+ return null;
460
+ }
327
461
  return /*#__PURE__*/_react["default"].createElement("div", {
328
462
  style: style
329
463
  }, renderInput(getInputStyles(theme, disabled, mark.disabled), label));
@@ -1 +1 @@
1
- {"version":3,"file":"mark-label.js","names":["_react","_interopRequireWildcard","require","_propTypes","_interopRequireDefault","_styles","_reactInputAutosize","_useDebounce","_plot","_renderUi","_labelSvgIcon","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","_typeof","has","get","set","_t","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","ownKeys","keys","getOwnPropertySymbols","filter","enumerable","push","apply","_objectSpread","arguments","length","forEach","_defineProperty2","getOwnPropertyDescriptors","defineProperties","DEBOUNCE_DELAY","StyledInputCorrect","styled","_ref2","theme","padding","spacing","borderRadius","fontSize","backgroundColor","color","defaults","WHITE","CORRECT_WITH_ICON","border","concat","StyledInputIncorrect","_ref3","INCORRECT_WITH_ICON","StyledInputMissing","_ref4","MISSING_WITH_ICON","fontWeight","StyledIncorrect","getInputStyles","disabled","markDisabled","fontFamily","typography","PRIMARY_DARK","SECONDARY","WebkitOpacity","undefined","WebkitTextFillColor","getStudentInputStyles","position","exports","graphProps","mark","rect","width","height","scale","domain","range","shift","rightEdge","x","bottomEdge","y","h","max","v","min","coordinates","left","top","LabelInput","_ref5","_ref","externalInputRef","label","inputStyle","onChange","onBlur","createElement","inputRef","value","propTypes","PropTypes","func","string","bool","object","MarkLabel","props","inputNode","useRef","_useState","useState","_useState2","_slicedToArray2","input","setInput","useCallback","node","current","useTheme","autoFocus","_useState3","_useState4","setLabel","correctness","correctnesslabel","correctlabel","isFocused","document","activeElement","isNewLabel","target","handleBlur","debouncedLabel","useDebounce","useEffect","focus","caret","setSelectionRange","getBoundingClientRect","pos","leftTop","style","pointerEvents","display","alignItems","gap","secondLabelStyle","disabledInput","renderInput","labelValue","studentInputStyle","type","Fragment","marginLeft","types","GraphPropsType","_default"],"sources":["../src/mark-label.jsx"],"sourcesContent":["import React, { useCallback, useEffect, useRef, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { styled, useTheme } from '@mui/material/styles';\nimport AutosizeInput from 'react-input-autosize';\nimport { useDebounce } from './use-debounce';\nimport { types } from '@pie-lib/plot';\nimport { color } from '@pie-lib/render-ui';\nimport SvgIcon from './label-svg-icon';\n\nconst DEBOUNCE_DELAY = 500;\n\nconst StyledInputCorrect = styled('div')(({ theme }) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.CORRECT_WITH_ICON,\n border: `solid 1px ${color.defaults.CORRECT_WITH_ICON}`,\n}));\n\nconst StyledInputIncorrect = styled('div')(({ theme }) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.INCORRECT_WITH_ICON,\n border: `solid 1px ${color.defaults.INCORRECT_WITH_ICON}`,\n}));\n\nconst StyledInputMissing = styled('div')(({ theme }) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.MISSING_WITH_ICON,\n border: `solid 1px ${color.defaults.MISSING_WITH_ICON}`,\n fontWeight: 'bold',\n}));\n\nconst StyledIncorrect = styled('div')(() => ({\n float: 'right',\n padding: 0,\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.INCORRECT_WITH_ICON,\n fontWeight: 'bold',\n}));\n\nconst getInputStyles = (theme, disabled, markDisabled) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n fontFamily: theme.typography.fontFamily,\n fontSize: '10px',\n border: disabled\n ? `solid 1px ${color.defaults.PRIMARY_DARK}`\n : markDisabled\n ? `solid 1px ${color.disabled()}`\n : `solid 1px ${color.defaults.SECONDARY}`,\n borderRadius: '3px',\n color: markDisabled ? color.disabled() : color.defaults.PRIMARY_DARK,\n backgroundColor: color.defaults.WHITE,\n WebkitOpacity: disabled ? '1' : undefined,\n WebkitTextFillColor: markDisabled ? color.disabled() : undefined,\n});\n\nconst getStudentInputStyles = () => ({\n padding: '0',\n border: 'none',\n color: 'inherit',\n fontWeight: 'bold',\n});\n\nexport const position = (graphProps, mark, rect = { width: 0, height: 0 }) => {\n const { scale, domain, range } = graphProps;\n const shift = 5;\n\n const rightEdge = scale.x(mark.x) + rect.width + shift;\n const bottomEdge = scale.y(mark.y) + rect.height + shift;\n\n const h = rightEdge >= scale.x(domain.max) ? 'left' : 'right';\n const v = bottomEdge >= scale.y(range.min) ? 'top' : 'bottom';\n\n return `${v}-${h}`;\n};\n\nexport const coordinates = (graphProps, mark, rect = { width: 0, height: 0 }, position) => {\n const { scale } = graphProps;\n const shift = 5;\n\n switch (position) {\n case 'bottom-right':\n return { left: scale.x(mark.x) + shift, top: scale.y(mark.y) + shift };\n case 'bottom-left':\n return { left: scale.x(mark.x) - shift - rect.width, top: scale.y(mark.y) + shift };\n case 'top-left':\n return { left: scale.x(mark.x) - shift - rect.width, top: scale.y(mark.y) - shift - rect.height };\n case 'top-right':\n return { left: scale.x(mark.x) + shift, top: scale.y(mark.y) - shift - rect.height };\n default:\n return {};\n }\n};\n\nconst LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, onChange, onBlur }) => (\n <AutosizeInput\n inputRef={(r) => {\n _ref(r);\n externalInputRef(r);\n }}\n disabled={disabled}\n inputStyle={inputStyle}\n value={label}\n onChange={onChange}\n onBlur={onBlur}\n />\n);\n\nLabelInput.propTypes = {\n _ref: PropTypes.func,\n externalInputRef: PropTypes.func,\n label: PropTypes.string,\n disabled: PropTypes.bool,\n inputStyle: PropTypes.object,\n onChange: PropTypes.func,\n onBlur: PropTypes.func,\n};\n\nexport const MarkLabel = (props) => {\n // the node is kept in a ref as well as in the state: the state is needed to reposition the label\n // once it has a size, the ref is what the focus is asserted on - it is never a render behind\n const inputNode = useRef(null);\n const [input, setInput] = useState(null);\n const _ref = useCallback((node) => {\n inputNode.current = node;\n setInput(node);\n }, []);\n const theme = useTheme();\n\n const { mark, graphProps, disabled, autoFocus, inputRef: externalInputRef } = props;\n\n const [label, setLabel] = useState(mark.label);\n const { correctness, correctnesslabel, correctlabel } = mark;\n\n const isFocused = () =>\n !!inputNode.current && typeof document !== 'undefined' && document.activeElement === inputNode.current;\n\n // A label that has just been added is empty and its input is enabled - label mode has to be on\n // for that to happen and it always starts off, so a mark that was loaded with an empty label\n // cannot grab the focus. This is read from the mark rather than taken from the autoFocus prop\n // because the tool that asked for the label can be remounted by the marks coming back from the\n // host (async model save) before the input ever renders, and its autoFocus goes with it.\n const isNewLabel = useRef(mark.label === '' && !disabled && !mark.disabled);\n\n const onChange = (e) => setLabel(e.target.value);\n\n const handleBlur = useCallback(() => {\n isNewLabel.current = false;\n\n if (label === '') {\n props.onChange('');\n }\n\n // lets the tool know that this label is not being edited anymore, so it stops auto focusing it\n if (props.onBlur) {\n props.onBlur();\n }\n }, [label, props.onChange, props.onBlur]);\n\n const debouncedLabel = useDebounce(label, DEBOUNCE_DELAY);\n\n // useState only sets the value once, to synch props to state need useEffect.\n // While this input has the focus we keep what the user typed: the mark can come back from an\n // (async) model save with an older label and that would overwrite the characters typed meanwhile.\n useEffect(() => {\n if (isFocused()) {\n return;\n }\n\n setLabel(mark.label);\n }, [mark.label]);\n\n // pick up the change to debouncedLabel and save it\n useEffect(() => {\n if (typeof debouncedLabel === 'string' && debouncedLabel !== mark.label) {\n props.onChange(debouncedLabel);\n }\n }, [debouncedLabel]);\n\n // A label that has just been added has to be focused so that it can be typed into right away.\n // This runs after every render on purpose - there is no reliable single moment to focus at:\n // the input shows up whenever the mark comes back from the (async) model save, and a remount\n // of the input (same save, replaced node) drops the focus without this component being told.\n // Asserting the focus on every render is what makes it stick; it is a no-op once the input has\n // it, so the caret the user placed is left alone.\n useEffect(() => {\n const node = inputNode.current;\n\n // node.disabled, not the disabled prop: a disabled input silently ignores focus()\n if ((!autoFocus && !isNewLabel.current) || !node || node.disabled || isFocused()) {\n return;\n }\n\n node.focus();\n\n // a new label is focused once, so it cannot fight over the focus with another one\n if (isFocused()) {\n isNewLabel.current = false;\n }\n\n // keep the caret at the end, otherwise typing continues in front of the existing label\n const caret = (node.value || '').length;\n\n if (node.setSelectionRange) {\n node.setSelectionRange(caret, caret);\n }\n });\n\n const rect = input ? input.getBoundingClientRect() : { width: 0, height: 0 };\n const pos = position(graphProps, mark, rect);\n const leftTop = coordinates(graphProps, mark, rect, pos);\n\n const style = {\n position: 'fixed',\n pointerEvents: 'auto',\n display: 'flex',\n alignItems: 'center',\n gap: '3px',\n ...leftTop,\n };\n\n const secondLabelStyle = {\n ...style,\n top: leftTop.top + 25,\n };\n\n const disabledInput = disabled || mark.disabled;\n\n const renderInput = (inputStyle, labelValue) => (\n <LabelInput\n _ref={_ref}\n externalInputRef={externalInputRef}\n label={labelValue}\n disabled={disabledInput}\n inputStyle={inputStyle}\n onChange={onChange}\n onBlur={handleBlur}\n />\n );\n\n const studentInputStyle = getStudentInputStyles();\n\n if (correctness === 'correct' && correctnesslabel === 'correct' && correctlabel) {\n return (\n <StyledInputCorrect style={style}>\n <SvgIcon type=\"correct\" />\n {renderInput(studentInputStyle, correctlabel)}\n </StyledInputCorrect>\n );\n }\n\n if (correctness === 'correct' && correctnesslabel === 'correct' && !correctlabel) {\n return null;\n }\n\n if (correctness === 'correct' && correctnesslabel === 'incorrect') {\n return (\n <>\n <StyledInputIncorrect style={style}>\n <SvgIcon type=\"incorrect\" />\n {label === '' ? (\n <SvgIcon type=\"empty\" style={{ marginLeft: '3px' }} />\n ) : (\n renderInput(studentInputStyle, label)\n )}\n </StyledInputIncorrect>\n <StyledInputMissing style={secondLabelStyle}>{renderInput(studentInputStyle, correctlabel)}</StyledInputMissing>\n </>\n );\n }\n\n if (correctness === 'missing') {\n return <StyledInputMissing style={style}>{renderInput(studentInputStyle, label)}</StyledInputMissing>;\n }\n\n if (correctness === 'incorrect') {\n return <StyledIncorrect style={style}>{renderInput(studentInputStyle, label)}</StyledIncorrect>;\n }\n\n return <div style={style}>{renderInput(getInputStyles(theme, disabled, mark.disabled), label)}</div>;\n};\n\nMarkLabel.propTypes = {\n autoFocus: PropTypes.bool,\n disabled: PropTypes.bool,\n onBlur: PropTypes.func,\n onChange: PropTypes.func,\n graphProps: types.GraphPropsType,\n inputRef: PropTypes.func,\n mark: PropTypes.object,\n};\n\nexport default MarkLabel;\n"],"mappings":";;;;;;;;;;AAAA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AACA,IAAAC,UAAA,GAAAC,sBAAA,CAAAF,OAAA;AACA,IAAAG,OAAA,GAAAH,OAAA;AACA,IAAAI,mBAAA,GAAAF,sBAAA,CAAAF,OAAA;AACA,IAAAK,YAAA,GAAAL,OAAA;AACA,IAAAM,KAAA,GAAAN,OAAA;AACA,IAAAO,SAAA,GAAAP,OAAA;AACA,IAAAQ,aAAA,GAAAN,sBAAA,CAAAF,OAAA;AAAuC,SAAAD,wBAAAU,CAAA,EAAAC,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAZ,uBAAA,YAAAA,wBAAAU,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,mBAAAT,CAAA,iBAAAA,CAAA,gBAAAU,OAAA,CAAAV,CAAA,0BAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAK,GAAA,CAAAX,CAAA,UAAAM,CAAA,CAAAM,GAAA,CAAAZ,CAAA,GAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,EAAAQ,CAAA,cAAAM,EAAA,IAAAd,CAAA,gBAAAc,EAAA,OAAAC,cAAA,CAAAC,IAAA,CAAAhB,CAAA,EAAAc,EAAA,OAAAP,CAAA,IAAAD,CAAA,GAAAW,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAnB,CAAA,EAAAc,EAAA,OAAAP,CAAA,CAAAK,GAAA,IAAAL,CAAA,CAAAM,GAAA,IAAAP,CAAA,CAAAE,CAAA,EAAAM,EAAA,EAAAP,CAAA,IAAAC,CAAA,CAAAM,EAAA,IAAAd,CAAA,CAAAc,EAAA,WAAAN,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAAA,SAAAmB,QAAApB,CAAA,EAAAG,CAAA,QAAAF,CAAA,GAAAgB,MAAA,CAAAI,IAAA,CAAArB,CAAA,OAAAiB,MAAA,CAAAK,qBAAA,QAAAhB,CAAA,GAAAW,MAAA,CAAAK,qBAAA,CAAAtB,CAAA,GAAAG,CAAA,KAAAG,CAAA,GAAAA,CAAA,CAAAiB,MAAA,WAAApB,CAAA,WAAAc,MAAA,CAAAE,wBAAA,CAAAnB,CAAA,EAAAG,CAAA,EAAAqB,UAAA,OAAAvB,CAAA,CAAAwB,IAAA,CAAAC,KAAA,CAAAzB,CAAA,EAAAK,CAAA,YAAAL,CAAA;AAAA,SAAA0B,cAAA3B,CAAA,aAAAG,CAAA,MAAAA,CAAA,GAAAyB,SAAA,CAAAC,MAAA,EAAA1B,CAAA,UAAAF,CAAA,WAAA2B,SAAA,CAAAzB,CAAA,IAAAyB,SAAA,CAAAzB,CAAA,QAAAA,CAAA,OAAAiB,OAAA,CAAAH,MAAA,CAAAhB,CAAA,OAAA6B,OAAA,WAAA3B,CAAA,QAAA4B,gBAAA,aAAA/B,CAAA,EAAAG,CAAA,EAAAF,CAAA,CAAAE,CAAA,SAAAc,MAAA,CAAAe,yBAAA,GAAAf,MAAA,CAAAgB,gBAAA,CAAAjC,CAAA,EAAAiB,MAAA,CAAAe,yBAAA,CAAA/B,CAAA,KAAAmB,OAAA,CAAAH,MAAA,CAAAhB,CAAA,GAAA6B,OAAA,WAAA3B,CAAA,IAAAc,MAAA,CAAAC,cAAA,CAAAlB,CAAA,EAAAG,CAAA,EAAAc,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAE,CAAA,iBAAAH,CAAA;AAEvC,IAAMkC,cAAc,GAAG,GAAG;AAE1B,IAAMC,kBAAkB,GAAG,IAAAC,cAAM,EAAC,KAAK,CAAC,CAAC,UAAAC,KAAA;EAAA,IAAGC,KAAK,GAAAD,KAAA,CAALC,KAAK;EAAA,OAAQ;IACvD,SAAO,OAAO;IACdC,OAAO,EAAED,KAAK,CAACE,OAAO,CAAC,GAAG,CAAC;IAC3BC,YAAY,EAAE,KAAK;IACnBC,QAAQ,EAAE,MAAM;IAChBC,eAAe,EAAEC,eAAK,CAACC,QAAQ,CAACC,KAAK;IACrCF,KAAK,EAAEA,eAAK,CAACC,QAAQ,CAACE,iBAAiB;IACvCC,MAAM,eAAAC,MAAA,CAAeL,eAAK,CAACC,QAAQ,CAACE,iBAAiB;EACvD,CAAC;AAAA,CAAC,CAAC;AAEH,IAAMG,oBAAoB,GAAG,IAAAd,cAAM,EAAC,KAAK,CAAC,CAAC,UAAAe,KAAA;EAAA,IAAGb,KAAK,GAAAa,KAAA,CAALb,KAAK;EAAA,OAAQ;IACzD,SAAO,OAAO;IACdC,OAAO,EAAED,KAAK,CAACE,OAAO,CAAC,GAAG,CAAC;IAC3BC,YAAY,EAAE,KAAK;IACnBC,QAAQ,EAAE,MAAM;IAChBC,eAAe,EAAEC,eAAK,CAACC,QAAQ,CAACC,KAAK;IACrCF,KAAK,EAAEA,eAAK,CAACC,QAAQ,CAACO,mBAAmB;IACzCJ,MAAM,eAAAC,MAAA,CAAeL,eAAK,CAACC,QAAQ,CAACO,mBAAmB;EACzD,CAAC;AAAA,CAAC,CAAC;AAEH,IAAMC,kBAAkB,GAAG,IAAAjB,cAAM,EAAC,KAAK,CAAC,CAAC,UAAAkB,KAAA;EAAA,IAAGhB,KAAK,GAAAgB,KAAA,CAALhB,KAAK;EAAA,OAAQ;IACvD,SAAO,OAAO;IACdC,OAAO,EAAED,KAAK,CAACE,OAAO,CAAC,GAAG,CAAC;IAC3BC,YAAY,EAAE,KAAK;IACnBC,QAAQ,EAAE,MAAM;IAChBC,eAAe,EAAEC,eAAK,CAACC,QAAQ,CAACC,KAAK;IACrCF,KAAK,EAAEA,eAAK,CAACC,QAAQ,CAACU,iBAAiB;IACvCP,MAAM,eAAAC,MAAA,CAAeL,eAAK,CAACC,QAAQ,CAACU,iBAAiB,CAAE;IACvDC,UAAU,EAAE;EACd,CAAC;AAAA,CAAC,CAAC;AAEH,IAAMC,eAAe,GAAG,IAAArB,cAAM,EAAC,KAAK,CAAC,CAAC;EAAA,OAAO;IAC3C,SAAO,OAAO;IACdG,OAAO,EAAE,CAAC;IACVE,YAAY,EAAE,KAAK;IACnBC,QAAQ,EAAE,MAAM;IAChBC,eAAe,EAAEC,eAAK,CAACC,QAAQ,CAACC,KAAK;IACrCF,KAAK,EAAEA,eAAK,CAACC,QAAQ,CAACO,mBAAmB;IACzCI,UAAU,EAAE;EACd,CAAC;AAAA,CAAC,CAAC;AAEH,IAAME,cAAc,GAAG,SAAjBA,cAAcA,CAAIpB,KAAK,EAAEqB,QAAQ,EAAEC,YAAY;EAAA,OAAM;IACzD,SAAO,OAAO;IACdrB,OAAO,EAAED,KAAK,CAACE,OAAO,CAAC,GAAG,CAAC;IAC3BqB,UAAU,EAAEvB,KAAK,CAACwB,UAAU,CAACD,UAAU;IACvCnB,QAAQ,EAAE,MAAM;IAChBM,MAAM,EAAEW,QAAQ,gBAAAV,MAAA,CACCL,eAAK,CAACC,QAAQ,CAACkB,YAAY,IACxCH,YAAY,gBAAAX,MAAA,CACGL,eAAK,CAACe,QAAQ,CAAC,CAAC,iBAAAV,MAAA,CAChBL,eAAK,CAACC,QAAQ,CAACmB,SAAS,CAAE;IAC7CvB,YAAY,EAAE,KAAK;IACnBG,KAAK,EAAEgB,YAAY,GAAGhB,eAAK,CAACe,QAAQ,CAAC,CAAC,GAAGf,eAAK,CAACC,QAAQ,CAACkB,YAAY;IACpEpB,eAAe,EAAEC,eAAK,CAACC,QAAQ,CAACC,KAAK;IACrCmB,aAAa,EAAEN,QAAQ,GAAG,GAAG,GAAGO,SAAS;IACzCC,mBAAmB,EAAEP,YAAY,GAAGhB,eAAK,CAACe,QAAQ,CAAC,CAAC,GAAGO;EACzD,CAAC;AAAA,CAAC;AAEF,IAAME,qBAAqB,GAAG,SAAxBA,qBAAqBA,CAAA;EAAA,OAAU;IACnC7B,OAAO,EAAE,GAAG;IACZS,MAAM,EAAE,MAAM;IACdJ,KAAK,EAAE,SAAS;IAChBY,UAAU,EAAE;EACd,CAAC;AAAA,CAAC;AAEK,IAAMa,QAAQ,GAAAC,OAAA,CAAAD,QAAA,GAAG,SAAXA,QAAQA,CAAIE,UAAU,EAAEC,IAAI,EAAqC;EAAA,IAAnCC,IAAI,GAAA7C,SAAA,CAAAC,MAAA,QAAAD,SAAA,QAAAsC,SAAA,GAAAtC,SAAA,MAAG;IAAE8C,KAAK,EAAE,CAAC;IAAEC,MAAM,EAAE;EAAE,CAAC;EACvE,IAAQC,KAAK,GAAoBL,UAAU,CAAnCK,KAAK;IAAEC,MAAM,GAAYN,UAAU,CAA5BM,MAAM;IAAEC,KAAK,GAAKP,UAAU,CAApBO,KAAK;EAC5B,IAAMC,KAAK,GAAG,CAAC;EAEf,IAAMC,SAAS,GAAGJ,KAAK,CAACK,CAAC,CAACT,IAAI,CAACS,CAAC,CAAC,GAAGR,IAAI,CAACC,KAAK,GAAGK,KAAK;EACtD,IAAMG,UAAU,GAAGN,KAAK,CAACO,CAAC,CAACX,IAAI,CAACW,CAAC,CAAC,GAAGV,IAAI,CAACE,MAAM,GAAGI,KAAK;EAExD,IAAMK,CAAC,GAAGJ,SAAS,IAAIJ,KAAK,CAACK,CAAC,CAACJ,MAAM,CAACQ,GAAG,CAAC,GAAG,MAAM,GAAG,OAAO;EAC7D,IAAMC,CAAC,GAAGJ,UAAU,IAAIN,KAAK,CAACO,CAAC,CAACL,KAAK,CAACS,GAAG,CAAC,GAAG,KAAK,GAAG,QAAQ;EAE7D,UAAAtC,MAAA,CAAUqC,CAAC,OAAArC,MAAA,CAAImC,CAAC;AAClB,CAAC;AAEM,IAAMI,WAAW,GAAAlB,OAAA,CAAAkB,WAAA,GAAG,SAAdA,WAAWA,CAAIjB,UAAU,EAAEC,IAAI,EAA+C;EAAA,IAA7CC,IAAI,GAAA7C,SAAA,CAAAC,MAAA,QAAAD,SAAA,QAAAsC,SAAA,GAAAtC,SAAA,MAAG;IAAE8C,KAAK,EAAE,CAAC;IAAEC,MAAM,EAAE;EAAE,CAAC;EAAA,IAAEN,QAAQ,GAAAzC,SAAA,CAAAC,MAAA,OAAAD,SAAA,MAAAsC,SAAA;EACpF,IAAQU,KAAK,GAAKL,UAAU,CAApBK,KAAK;EACb,IAAMG,KAAK,GAAG,CAAC;EAEf,QAAQV,QAAQ;IACd,KAAK,cAAc;MACjB,OAAO;QAAEoB,IAAI,EAAEb,KAAK,CAACK,CAAC,CAACT,IAAI,CAACS,CAAC,CAAC,GAAGF,KAAK;QAAEW,GAAG,EAAEd,KAAK,CAACO,CAAC,CAACX,IAAI,CAACW,CAAC,CAAC,GAAGJ;MAAM,CAAC;IACxE,KAAK,aAAa;MAChB,OAAO;QAAEU,IAAI,EAAEb,KAAK,CAACK,CAAC,CAACT,IAAI,CAACS,CAAC,CAAC,GAAGF,KAAK,GAAGN,IAAI,CAACC,KAAK;QAAEgB,GAAG,EAAEd,KAAK,CAACO,CAAC,CAACX,IAAI,CAACW,CAAC,CAAC,GAAGJ;MAAM,CAAC;IACrF,KAAK,UAAU;MACb,OAAO;QAAEU,IAAI,EAAEb,KAAK,CAACK,CAAC,CAACT,IAAI,CAACS,CAAC,CAAC,GAAGF,KAAK,GAAGN,IAAI,CAACC,KAAK;QAAEgB,GAAG,EAAEd,KAAK,CAACO,CAAC,CAACX,IAAI,CAACW,CAAC,CAAC,GAAGJ,KAAK,GAAGN,IAAI,CAACE;MAAO,CAAC;IACnG,KAAK,WAAW;MACd,OAAO;QAAEc,IAAI,EAAEb,KAAK,CAACK,CAAC,CAACT,IAAI,CAACS,CAAC,CAAC,GAAGF,KAAK;QAAEW,GAAG,EAAEd,KAAK,CAACO,CAAC,CAACX,IAAI,CAACW,CAAC,CAAC,GAAGJ,KAAK,GAAGN,IAAI,CAACE;MAAO,CAAC;IACtF;MACE,OAAO,CAAC,CAAC;EACb;AACF,CAAC;AAED,IAAMgB,UAAU,GAAG,SAAbA,UAAUA,CAAAC,KAAA;EAAA,IAAMC,IAAI,GAAAD,KAAA,CAAJC,IAAI;IAAEC,gBAAgB,GAAAF,KAAA,CAAhBE,gBAAgB;IAAEC,KAAK,GAAAH,KAAA,CAALG,KAAK;IAAEpC,QAAQ,GAAAiC,KAAA,CAARjC,QAAQ;IAAEqC,UAAU,GAAAJ,KAAA,CAAVI,UAAU;IAAEC,QAAQ,GAAAL,KAAA,CAARK,QAAQ;IAAEC,MAAM,GAAAN,KAAA,CAANM,MAAM;EAAA,oBACzF7G,MAAA,YAAA8G,aAAA,CAACxG,mBAAA,WAAa;IACZyG,QAAQ,EAAE,SAAVA,QAAQA,CAAGjG,CAAC,EAAK;MACf0F,IAAI,CAAC1F,CAAC,CAAC;MACP2F,gBAAgB,CAAC3F,CAAC,CAAC;IACrB,CAAE;IACFwD,QAAQ,EAAEA,QAAS;IACnBqC,UAAU,EAAEA,UAAW;IACvBK,KAAK,EAAEN,KAAM;IACbE,QAAQ,EAAEA,QAAS;IACnBC,MAAM,EAAEA;EAAO,CAChB,CAAC;AAAA,CACH;AAEDP,UAAU,CAACW,SAAS,GAAG;EACrBT,IAAI,EAAEU,qBAAS,CAACC,IAAI;EACpBV,gBAAgB,EAAES,qBAAS,CAACC,IAAI;EAChCT,KAAK,EAAEQ,qBAAS,CAACE,MAAM;EACvB9C,QAAQ,EAAE4C,qBAAS,CAACG,IAAI;EACxBV,UAAU,EAAEO,qBAAS,CAACI,MAAM;EAC5BV,QAAQ,EAAEM,qBAAS,CAACC,IAAI;EACxBN,MAAM,EAAEK,qBAAS,CAACC;AACpB,CAAC;AAEM,IAAMI,SAAS,GAAAtC,OAAA,CAAAsC,SAAA,GAAG,SAAZA,SAASA,CAAIC,KAAK,EAAK;EAClC;EACA;EACA,IAAMC,SAAS,GAAG,IAAAC,aAAM,EAAC,IAAI,CAAC;EAC9B,IAAAC,SAAA,GAA0B,IAAAC,eAAQ,EAAC,IAAI,CAAC;IAAAC,UAAA,OAAAC,eAAA,aAAAH,SAAA;IAAjCI,KAAK,GAAAF,UAAA;IAAEG,QAAQ,GAAAH,UAAA;EACtB,IAAMrB,IAAI,GAAG,IAAAyB,kBAAW,EAAC,UAACC,IAAI,EAAK;IACjCT,SAAS,CAACU,OAAO,GAAGD,IAAI;IACxBF,QAAQ,CAACE,IAAI,CAAC;EAChB,CAAC,EAAE,EAAE,CAAC;EACN,IAAMjF,KAAK,GAAG,IAAAmF,gBAAQ,EAAC,CAAC;EAExB,IAAQjD,IAAI,GAAkEqC,KAAK,CAA3ErC,IAAI;IAAED,UAAU,GAAsDsC,KAAK,CAArEtC,UAAU;IAAEZ,QAAQ,GAA4CkD,KAAK,CAAzDlD,QAAQ;IAAE+D,SAAS,GAAiCb,KAAK,CAA/Ca,SAAS;IAAY5B,gBAAgB,GAAKe,KAAK,CAApCT,QAAQ;EAEvD,IAAAuB,UAAA,GAA0B,IAAAV,eAAQ,EAACzC,IAAI,CAACuB,KAAK,CAAC;IAAA6B,UAAA,OAAAT,eAAA,aAAAQ,UAAA;IAAvC5B,KAAK,GAAA6B,UAAA;IAAEC,QAAQ,GAAAD,UAAA;EACtB,IAAQE,WAAW,GAAqCtD,IAAI,CAApDsD,WAAW;IAAEC,gBAAgB,GAAmBvD,IAAI,CAAvCuD,gBAAgB;IAAEC,YAAY,GAAKxD,IAAI,CAArBwD,YAAY;EAEnD,IAAMC,SAAS,GAAG,SAAZA,SAASA,CAAA;IAAA,OACb,CAAC,CAACnB,SAAS,CAACU,OAAO,IAAI,OAAOU,QAAQ,KAAK,WAAW,IAAIA,QAAQ,CAACC,aAAa,KAAKrB,SAAS,CAACU,OAAO;EAAA;;EAExG;EACA;EACA;EACA;EACA;EACA,IAAMY,UAAU,GAAG,IAAArB,aAAM,EAACvC,IAAI,CAACuB,KAAK,KAAK,EAAE,IAAI,CAACpC,QAAQ,IAAI,CAACa,IAAI,CAACb,QAAQ,CAAC;EAE3E,IAAMsC,QAAQ,GAAG,SAAXA,QAAQA,CAAIjG,CAAC;IAAA,OAAK6H,QAAQ,CAAC7H,CAAC,CAACqI,MAAM,CAAChC,KAAK,CAAC;EAAA;EAEhD,IAAMiC,UAAU,GAAG,IAAAhB,kBAAW,EAAC,YAAM;IACnCc,UAAU,CAACZ,OAAO,GAAG,KAAK;IAE1B,IAAIzB,KAAK,KAAK,EAAE,EAAE;MAChBc,KAAK,CAACZ,QAAQ,CAAC,EAAE,CAAC;IACpB;;IAEA;IACA,IAAIY,KAAK,CAACX,MAAM,EAAE;MAChBW,KAAK,CAACX,MAAM,CAAC,CAAC;IAChB;EACF,CAAC,EAAE,CAACH,KAAK,EAAEc,KAAK,CAACZ,QAAQ,EAAEY,KAAK,CAACX,MAAM,CAAC,CAAC;EAEzC,IAAMqC,cAAc,GAAG,IAAAC,wBAAW,EAACzC,KAAK,EAAE7D,cAAc,CAAC;;EAEzD;EACA;EACA;EACA,IAAAuG,gBAAS,EAAC,YAAM;IACd,IAAIR,SAAS,CAAC,CAAC,EAAE;MACf;IACF;IAEAJ,QAAQ,CAACrD,IAAI,CAACuB,KAAK,CAAC;EACtB,CAAC,EAAE,CAACvB,IAAI,CAACuB,KAAK,CAAC,CAAC;;EAEhB;EACA,IAAA0C,gBAAS,EAAC,YAAM;IACd,IAAI,OAAOF,cAAc,KAAK,QAAQ,IAAIA,cAAc,KAAK/D,IAAI,CAACuB,KAAK,EAAE;MACvEc,KAAK,CAACZ,QAAQ,CAACsC,cAAc,CAAC;IAChC;EACF,CAAC,EAAE,CAACA,cAAc,CAAC,CAAC;;EAEpB;EACA;EACA;EACA;EACA;EACA;EACA,IAAAE,gBAAS,EAAC,YAAM;IACd,IAAMlB,IAAI,GAAGT,SAAS,CAACU,OAAO;;IAE9B;IACA,IAAK,CAACE,SAAS,IAAI,CAACU,UAAU,CAACZ,OAAO,IAAK,CAACD,IAAI,IAAIA,IAAI,CAAC5D,QAAQ,IAAIsE,SAAS,CAAC,CAAC,EAAE;MAChF;IACF;IAEAV,IAAI,CAACmB,KAAK,CAAC,CAAC;;IAEZ;IACA,IAAIT,SAAS,CAAC,CAAC,EAAE;MACfG,UAAU,CAACZ,OAAO,GAAG,KAAK;IAC5B;;IAEA;IACA,IAAMmB,KAAK,GAAG,CAACpB,IAAI,CAAClB,KAAK,IAAI,EAAE,EAAExE,MAAM;IAEvC,IAAI0F,IAAI,CAACqB,iBAAiB,EAAE;MAC1BrB,IAAI,CAACqB,iBAAiB,CAACD,KAAK,EAAEA,KAAK,CAAC;IACtC;EACF,CAAC,CAAC;EAEF,IAAMlE,IAAI,GAAG2C,KAAK,GAAGA,KAAK,CAACyB,qBAAqB,CAAC,CAAC,GAAG;IAAEnE,KAAK,EAAE,CAAC;IAAEC,MAAM,EAAE;EAAE,CAAC;EAC5E,IAAMmE,GAAG,GAAGzE,QAAQ,CAACE,UAAU,EAAEC,IAAI,EAAEC,IAAI,CAAC;EAC5C,IAAMsE,OAAO,GAAGvD,WAAW,CAACjB,UAAU,EAAEC,IAAI,EAAEC,IAAI,EAAEqE,GAAG,CAAC;EAExD,IAAME,KAAK,GAAArH,aAAA;IACT0C,QAAQ,EAAE,OAAO;IACjB4E,aAAa,EAAE,MAAM;IACrBC,OAAO,EAAE,MAAM;IACfC,UAAU,EAAE,QAAQ;IACpBC,GAAG,EAAE;EAAK,GACPL,OAAO,CACX;EAED,IAAMM,gBAAgB,GAAA1H,aAAA,CAAAA,aAAA,KACjBqH,KAAK;IACRtD,GAAG,EAAEqD,OAAO,CAACrD,GAAG,GAAG;EAAE,EACtB;EAED,IAAM4D,aAAa,GAAG3F,QAAQ,IAAIa,IAAI,CAACb,QAAQ;EAE/C,IAAM4F,WAAW,GAAG,SAAdA,WAAWA,CAAIvD,UAAU,EAAEwD,UAAU;IAAA,oBACzCnK,MAAA,YAAA8G,aAAA,CAACR,UAAU;MACTE,IAAI,EAAEA,IAAK;MACXC,gBAAgB,EAAEA,gBAAiB;MACnCC,KAAK,EAAEyD,UAAW;MAClB7F,QAAQ,EAAE2F,aAAc;MACxBtD,UAAU,EAAEA,UAAW;MACvBC,QAAQ,EAAEA,QAAS;MACnBC,MAAM,EAAEoC;IAAW,CACpB,CAAC;EAAA,CACH;EAED,IAAMmB,iBAAiB,GAAGrF,qBAAqB,CAAC,CAAC;EAEjD,IAAI0D,WAAW,KAAK,SAAS,IAAIC,gBAAgB,KAAK,SAAS,IAAIC,YAAY,EAAE;IAC/E,oBACE3I,MAAA,YAAA8G,aAAA,CAAChE,kBAAkB;MAAC6G,KAAK,EAAEA;IAAM,gBAC/B3J,MAAA,YAAA8G,aAAA,CAACpG,aAAA,WAAO;MAAC2J,IAAI,EAAC;IAAS,CAAE,CAAC,EACzBH,WAAW,CAACE,iBAAiB,EAAEzB,YAAY,CAC1B,CAAC;EAEzB;EAEA,IAAIF,WAAW,KAAK,SAAS,IAAIC,gBAAgB,KAAK,SAAS,IAAI,CAACC,YAAY,EAAE;IAChF,OAAO,IAAI;EACb;EAEA,IAAIF,WAAW,KAAK,SAAS,IAAIC,gBAAgB,KAAK,WAAW,EAAE;IACjE,oBACE1I,MAAA,YAAA8G,aAAA,CAAA9G,MAAA,YAAAsK,QAAA,qBACEtK,MAAA,YAAA8G,aAAA,CAACjD,oBAAoB;MAAC8F,KAAK,EAAEA;IAAM,gBACjC3J,MAAA,YAAA8G,aAAA,CAACpG,aAAA,WAAO;MAAC2J,IAAI,EAAC;IAAW,CAAE,CAAC,EAC3B3D,KAAK,KAAK,EAAE,gBACX1G,MAAA,YAAA8G,aAAA,CAACpG,aAAA,WAAO;MAAC2J,IAAI,EAAC,OAAO;MAACV,KAAK,EAAE;QAAEY,UAAU,EAAE;MAAM;IAAE,CAAE,CAAC,GAEtDL,WAAW,CAACE,iBAAiB,EAAE1D,KAAK,CAElB,CAAC,eACvB1G,MAAA,YAAA8G,aAAA,CAAC9C,kBAAkB;MAAC2F,KAAK,EAAEK;IAAiB,GAAEE,WAAW,CAACE,iBAAiB,EAAEzB,YAAY,CAAsB,CAC/G,CAAC;EAEP;EAEA,IAAIF,WAAW,KAAK,SAAS,EAAE;IAC7B,oBAAOzI,MAAA,YAAA8G,aAAA,CAAC9C,kBAAkB;MAAC2F,KAAK,EAAEA;IAAM,GAAEO,WAAW,CAACE,iBAAiB,EAAE1D,KAAK,CAAsB,CAAC;EACvG;EAEA,IAAI+B,WAAW,KAAK,WAAW,EAAE;IAC/B,oBAAOzI,MAAA,YAAA8G,aAAA,CAAC1C,eAAe;MAACuF,KAAK,EAAEA;IAAM,GAAEO,WAAW,CAACE,iBAAiB,EAAE1D,KAAK,CAAmB,CAAC;EACjG;EAEA,oBAAO1G,MAAA,YAAA8G,aAAA;IAAK6C,KAAK,EAAEA;EAAM,GAAEO,WAAW,CAAC7F,cAAc,CAACpB,KAAK,EAAEqB,QAAQ,EAAEa,IAAI,CAACb,QAAQ,CAAC,EAAEoC,KAAK,CAAO,CAAC;AACtG,CAAC;AAEDa,SAAS,CAACN,SAAS,GAAG;EACpBoB,SAAS,EAAEnB,qBAAS,CAACG,IAAI;EACzB/C,QAAQ,EAAE4C,qBAAS,CAACG,IAAI;EACxBR,MAAM,EAAEK,qBAAS,CAACC,IAAI;EACtBP,QAAQ,EAAEM,qBAAS,CAACC,IAAI;EACxBjC,UAAU,EAAEsF,WAAK,CAACC,cAAc;EAChC1D,QAAQ,EAAEG,qBAAS,CAACC,IAAI;EACxBhC,IAAI,EAAE+B,qBAAS,CAACI;AAClB,CAAC;AAAC,IAAAoD,QAAA,GAAAzF,OAAA,cAEasC,SAAS","ignoreList":[]}
1
+ {"version":3,"file":"mark-label.js","names":["_react","_interopRequireWildcard","require","_propTypes","_interopRequireDefault","_styles","_reactInputAutosize","_useDebounce","_plot","_renderUi","_labelSvgIcon","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","_typeof","has","get","set","_t","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","ownKeys","keys","getOwnPropertySymbols","filter","enumerable","push","apply","_objectSpread","arguments","length","forEach","_defineProperty2","getOwnPropertyDescriptors","defineProperties","DEBOUNCE_DELAY","INSIST_ON_FOCUS_FOR","INSIST_AT","MAX_CARET_CYCLES","EMPTY_INPUT_WIDTH","StyledInputCorrect","styled","_ref2","theme","padding","spacing","borderRadius","fontSize","backgroundColor","color","defaults","WHITE","CORRECT_WITH_ICON","border","concat","StyledInputIncorrect","_ref3","INCORRECT_WITH_ICON","StyledInputMissing","_ref4","MISSING_WITH_ICON","fontWeight","StyledIncorrect","getInputStyles","disabled","markDisabled","fontFamily","typography","PRIMARY_DARK","SECONDARY","WebkitOpacity","undefined","WebkitTextFillColor","getStudentInputStyles","position","exports","graphProps","mark","rect","width","height","scale","domain","range","shift","rightEdge","x","bottomEdge","y","h","max","v","min","coordinates","left","top","caretIsInside","node","document","getSelection","selection","rangeCount","anchorNode","anchor","parentNode","contains","LabelInput","_ref5","_ref","externalInputRef","label","inputStyle","minWidth","onChange","onBlur","createElement","inputRef","value","propTypes","PropTypes","func","string","bool","object","number","MarkLabel","props","inputNode","useRef","_useState","useState","_useState2","_slicedToArray2","input","setInput","useCallback","current","useTheme","autoFocus","_useState3","_useState4","setLabel","correctness","correctnesslabel","correctlabel","isFocused","activeElement","isNewLabel","insistUntil","lastSaved","insisting","Date","now","windowAway","selfFocusing","caretCycles","target","saveLabel","handleBlur","debouncedLabel","useDebounce","useEffect","focusInput","wasFocused","focus","setSelectionRange","keepCaret","start","selectionStart","end","selectionEnd","blur","timers","map","delay","setTimeout","clearTimeout","onWindowBlur","onWindowFocus","window","addEventListener","removeEventListener","getBoundingClientRect","pos","leftTop","style","pointerEvents","display","alignItems","gap","secondLabelStyle","disabledInput","renderInput","labelValue","studentInputStyle","type","Fragment","marginLeft","types","GraphPropsType","_default"],"sources":["../src/mark-label.jsx"],"sourcesContent":["import React, { useCallback, useEffect, useRef, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { styled, useTheme } from '@mui/material/styles';\nimport AutosizeInput from 'react-input-autosize';\nimport { useDebounce } from './use-debounce';\nimport { types } from '@pie-lib/plot';\nimport { color } from '@pie-lib/render-ui';\nimport SvgIcon from './label-svg-icon';\n\nconst DEBOUNCE_DELAY = 500;\n// A new label insists on the focus and the caret for this long, and again on each of the ticks\n// below. No single moment is reliable: the authoring host re-renders the whole element while it\n// saves, and the input is replaced under this component more than once. Nothing typed yet is what\n// tells a transient blur apart from the user leaving the label.\nconst INSIST_ON_FOCUS_FOR = 500;\nconst INSIST_AT = [0, 50, 120, 250, 400, 700, 1000];\n// how many times a label will do the heavier focus cycle to get a caret back\nconst MAX_CARET_CYCLES = 3;\n// how wide an editable label input is while it is empty\nconst EMPTY_INPUT_WIDTH = 30;\n\nconst StyledInputCorrect = styled('div')(({ theme }) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.CORRECT_WITH_ICON,\n border: `solid 1px ${color.defaults.CORRECT_WITH_ICON}`,\n}));\n\nconst StyledInputIncorrect = styled('div')(({ theme }) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.INCORRECT_WITH_ICON,\n border: `solid 1px ${color.defaults.INCORRECT_WITH_ICON}`,\n}));\n\nconst StyledInputMissing = styled('div')(({ theme }) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.MISSING_WITH_ICON,\n border: `solid 1px ${color.defaults.MISSING_WITH_ICON}`,\n fontWeight: 'bold',\n}));\n\nconst StyledIncorrect = styled('div')(() => ({\n float: 'right',\n padding: 0,\n borderRadius: '4px',\n fontSize: '10px',\n backgroundColor: color.defaults.WHITE,\n color: color.defaults.INCORRECT_WITH_ICON,\n fontWeight: 'bold',\n}));\n\nconst getInputStyles = (theme, disabled, markDisabled) => ({\n float: 'right',\n padding: theme.spacing(0.5),\n fontFamily: theme.typography.fontFamily,\n fontSize: '10px',\n border: disabled\n ? `solid 1px ${color.defaults.PRIMARY_DARK}`\n : markDisabled\n ? `solid 1px ${color.disabled()}`\n : `solid 1px ${color.defaults.SECONDARY}`,\n borderRadius: '3px',\n color: markDisabled ? color.disabled() : color.defaults.PRIMARY_DARK,\n backgroundColor: color.defaults.WHITE,\n WebkitOpacity: disabled ? '1' : undefined,\n WebkitTextFillColor: markDisabled ? color.disabled() : undefined,\n});\n\nconst getStudentInputStyles = () => ({\n padding: '0',\n border: 'none',\n color: 'inherit',\n fontWeight: 'bold',\n});\n\nexport const position = (graphProps, mark, rect = { width: 0, height: 0 }) => {\n const { scale, domain, range } = graphProps;\n const shift = 5;\n\n const rightEdge = scale.x(mark.x) + rect.width + shift;\n const bottomEdge = scale.y(mark.y) + rect.height + shift;\n\n const h = rightEdge >= scale.x(domain.max) ? 'left' : 'right';\n const v = bottomEdge >= scale.y(range.min) ? 'top' : 'bottom';\n\n return `${v}-${h}`;\n};\n\nexport const coordinates = (graphProps, mark, rect = { width: 0, height: 0 }, position) => {\n const { scale } = graphProps;\n const shift = 5;\n\n switch (position) {\n case 'bottom-right':\n return { left: scale.x(mark.x) + shift, top: scale.y(mark.y) + shift };\n case 'bottom-left':\n return { left: scale.x(mark.x) - shift - rect.width, top: scale.y(mark.y) + shift };\n case 'top-left':\n return { left: scale.x(mark.x) - shift - rect.width, top: scale.y(mark.y) - shift - rect.height };\n case 'top-right':\n return { left: scale.x(mark.x) + shift, top: scale.y(mark.y) - shift - rect.height };\n default:\n return {};\n }\n};\n\n/**\n * Whether the document's caret is in this input. The characters land at the caret, not at the\n * focused node - an input can be the active element with no caret anywhere, and then keypress fires\n * and nothing is inserted, with nothing in the page cancelling anything.\n */\nconst caretIsInside = (node) => {\n if (typeof document === 'undefined' || !document.getSelection) {\n return true;\n }\n\n const selection = document.getSelection();\n\n if (!selection || !selection.rangeCount || !selection.anchorNode) {\n return false;\n }\n\n const anchor = selection.anchorNode;\n\n return anchor === node || anchor === node.parentNode || node.contains(anchor);\n};\n\nconst LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, minWidth, onChange, onBlur }) => (\n <AutosizeInput\n inputRef={(r) => {\n _ref(r);\n externalInputRef(r);\n }}\n disabled={disabled}\n inputStyle={inputStyle}\n minWidth={minWidth}\n value={label}\n onChange={onChange}\n onBlur={onBlur}\n />\n);\n\nLabelInput.propTypes = {\n _ref: PropTypes.func,\n externalInputRef: PropTypes.func,\n label: PropTypes.string,\n disabled: PropTypes.bool,\n inputStyle: PropTypes.object,\n minWidth: PropTypes.number,\n onChange: PropTypes.func,\n onBlur: PropTypes.func,\n};\n\nexport const MarkLabel = (props) => {\n // the state repositions the label once the input has a size, the ref is never a render behind\n const inputNode = useRef(null);\n const [input, setInput] = useState(null);\n const _ref = useCallback((node) => {\n inputNode.current = node;\n setInput(node);\n }, []);\n const theme = useTheme();\n\n const { mark, graphProps, disabled, autoFocus, inputRef: externalInputRef } = props;\n\n const [label, setLabel] = useState(mark.label);\n const { correctness, correctnesslabel, correctlabel } = mark;\n\n const isFocused = () =>\n !!inputNode.current && typeof document !== 'undefined' && document.activeElement === inputNode.current;\n\n // an empty label with an enabled input is one the user just added: label mode has to be on and it\n // always starts off. Read from the mark, not from autoFocus, because the tool that asked for the\n // label can be remounted by the marks coming back from the host before the input renders.\n const isNewLabel = useRef(mark.label === '' && !disabled && !mark.disabled);\n const insistUntil = useRef(0);\n const lastSaved = useRef(mark.label);\n const insisting = () => Date.now() < insistUntil.current && !(inputNode.current && inputNode.current.value);\n // the whole window losing the focus is not the user leaving the label - another window, the\n // devtools, or the host moving the focus while it saves. The window blur arrives before the\n // input's own blur, so this flag is set by the time the label is asked to give itself up.\n const windowAway = useRef(false);\n // our own blur, from the focus cycle that puts a missing caret back\n const selfFocusing = useRef(false);\n const caretCycles = useRef(0);\n\n const onChange = (e) => setLabel(e.target.value);\n\n const saveLabel = (value) => {\n if (value === lastSaved.current) {\n return;\n }\n\n lastSaved.current = value;\n props.onChange(value);\n };\n\n const handleBlur = useCallback(() => {\n // in the DNA env the focus is moved while the model is saved - not the user leaving the label\n if (insisting() || windowAway.current || selfFocusing.current) {\n return;\n }\n\n isNewLabel.current = false;\n\n if (label === '') {\n props.onChange('');\n } else if (label !== mark.label) {\n // the debounce can still be pending, and once the focus is gone an echo of the model would\n // overwrite what was typed before it fires\n saveLabel(label);\n }\n\n // lets the tool know that this label is not being edited anymore, so it stops auto focusing it\n if (props.onBlur) {\n props.onBlur();\n }\n }, [label, mark.label, props.onChange, props.onBlur]);\n\n const debouncedLabel = useDebounce(label, DEBOUNCE_DELAY);\n\n // props to state, but not while focused: the mark can come back from an async model save with an\n // older label and overwrite what was typed meanwhile\n useEffect(() => {\n if (isFocused()) {\n return;\n }\n\n setLabel(mark.label);\n }, [mark.label]);\n\n // pick up the change to debouncedLabel and save it\n useEffect(() => {\n if (typeof debouncedLabel === 'string' && debouncedLabel !== mark.label) {\n saveLabel(debouncedLabel);\n }\n }, [debouncedLabel]);\n\n const focusInput = () => {\n const node = inputNode.current;\n\n // node.disabled, not the disabled prop: a disabled input silently ignores focus()\n if ((!autoFocus && !isNewLabel.current && !insisting()) || !node || node.disabled) {\n return;\n }\n\n const wasFocused = isFocused();\n\n if (!wasFocused) {\n node.focus();\n\n // a new label is focused once, so it cannot fight over the focus with another one\n if (isFocused()) {\n isNewLabel.current = false;\n }\n }\n\n if (!node.setSelectionRange) {\n return;\n }\n\n // a label that was already being edited keeps the caret the user put in it; one that has just\n // taken the focus gets it at the end, otherwise typing continues in front of the existing label\n const keepCaret = wasFocused && node.value;\n const start = keepCaret ? node.selectionStart : (node.value || '').length;\n const end = keepCaret ? node.selectionEnd : start;\n\n // The characters land at the document's caret, not at the focused node. An input focused while\n // it was being replaced, or while the window was away, ends up as the active element with no\n // caret anywhere: keypress fires and nothing is inserted, with nothing in the page cancelling\n // anything. Re-asserting the range is what puts the caret back.\n if (!caretIsInside(node)) {\n node.setSelectionRange(start, end);\n }\n\n // and if it still is not there, a focus cycle is the heavier way to get it. Every focusInput\n // call is another go at it - after each render, on each insist tick, and when the window comes\n // back - up to a few times per label, since the state comes back as the input is replaced.\n if (caretIsInside(node) || caretCycles.current >= MAX_CARET_CYCLES) {\n return;\n }\n\n caretCycles.current += 1;\n // our own blur, so the label is not given up over it\n selfFocusing.current = true;\n node.blur();\n node.focus();\n node.setSelectionRange(start, end);\n selfFocusing.current = false;\n };\n\n // after every render: the input can appear a render or more later (the mark comes back from an\n // async model save) and a replaced node loses the focus without this component being told\n useEffect(() => focusInput());\n\n // and again on a timer, for the DNA env, where the focus is moved once the renders are over. The\n // original code focused from a setTimeout, which is what let it survive that.\n useEffect(() => {\n if (!autoFocus && !isNewLabel.current) {\n return;\n }\n\n if (!insistUntil.current) {\n insistUntil.current = Date.now() + INSIST_ON_FOCUS_FOR;\n }\n\n const timers = INSIST_AT.map((delay) => setTimeout(focusInput, delay));\n\n return () => timers.forEach((t) => clearTimeout(t));\n }, [autoFocus]);\n\n // the blur that came with the window going away is ignored, so the label is still the one being\n // edited when the window comes back and it takes the focus, and its selection, back\n useEffect(() => {\n const onWindowBlur = () => {\n windowAway.current = true;\n };\n\n const onWindowFocus = () => {\n // coming back only means something if the window went away: moving the focus from one element\n // to another inside the page reaches window too in some implementations\n if (!windowAway.current) {\n return;\n }\n\n windowAway.current = false;\n focusInput();\n };\n\n window.addEventListener('blur', onWindowBlur);\n window.addEventListener('focus', onWindowFocus);\n\n return () => {\n window.removeEventListener('blur', onWindowBlur);\n window.removeEventListener('focus', onWindowFocus);\n };\n }, [autoFocus]);\n\n const rect = input ? input.getBoundingClientRect() : { width: 0, height: 0 };\n const pos = position(graphProps, mark, rect);\n const leftTop = coordinates(graphProps, mark, rect, pos);\n\n const style = {\n position: 'fixed',\n pointerEvents: 'auto',\n display: 'flex',\n alignItems: 'center',\n gap: '3px',\n ...leftTop,\n };\n\n const secondLabelStyle = {\n ...style,\n top: leftTop.top + 25,\n };\n\n const disabledInput = disabled || mark.disabled;\n\n const renderInput = (inputStyle, labelValue) => (\n <LabelInput\n _ref={_ref}\n externalInputRef={externalInputRef}\n label={labelValue}\n disabled={disabledInput}\n inputStyle={inputStyle}\n // an editable label sizes itself to its content, so an empty one is 2px wide: enough for the\n // caret, not enough for anybody to see that the label is waiting to be typed into. Chrome\n // does not blink the caret of an input inside an svg foreignObject, it only paints a hairline,\n // so the box itself has to say that it has the focus.\n minWidth={disabledInput ? 1 : EMPTY_INPUT_WIDTH}\n onChange={onChange}\n onBlur={handleBlur}\n />\n );\n\n const studentInputStyle = getStudentInputStyles();\n\n if (correctness === 'correct' && correctnesslabel === 'correct' && correctlabel) {\n return (\n <StyledInputCorrect style={style}>\n <SvgIcon type=\"correct\" />\n {renderInput(studentInputStyle, correctlabel)}\n </StyledInputCorrect>\n );\n }\n\n if (correctness === 'correct' && correctnesslabel === 'correct' && !correctlabel) {\n return null;\n }\n\n if (correctness === 'correct' && correctnesslabel === 'incorrect') {\n return (\n <>\n <StyledInputIncorrect style={style}>\n <SvgIcon type=\"incorrect\" />\n {label === '' ? (\n <SvgIcon type=\"empty\" style={{ marginLeft: '3px' }} />\n ) : (\n renderInput(studentInputStyle, label)\n )}\n </StyledInputIncorrect>\n <StyledInputMissing style={secondLabelStyle}>{renderInput(studentInputStyle, correctlabel)}</StyledInputMissing>\n </>\n );\n }\n\n if (correctness === 'missing') {\n return <StyledInputMissing style={style}>{renderInput(studentInputStyle, label)}</StyledInputMissing>;\n }\n\n if (correctness === 'incorrect') {\n return <StyledIncorrect style={style}>{renderInput(studentInputStyle, label)}</StyledIncorrect>;\n }\n\n // an empty label that cannot be edited is just an empty box - the config draws the background\n // marks into every correct answer graph too\n if (disabledInput && !label) {\n return null;\n }\n\n return <div style={style}>{renderInput(getInputStyles(theme, disabled, mark.disabled), label)}</div>;\n};\n\nMarkLabel.propTypes = {\n autoFocus: PropTypes.bool,\n disabled: PropTypes.bool,\n onBlur: PropTypes.func,\n onChange: PropTypes.func,\n graphProps: types.GraphPropsType,\n inputRef: PropTypes.func,\n mark: PropTypes.object,\n};\n\nexport default MarkLabel;\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "4.0.17",
6
+ "version": "4.0.19",
7
7
  "description": "Graphing components",
8
8
  "keywords": [
9
9
  "react",
@@ -22,11 +22,11 @@
22
22
  "@mapbox/point-geometry": "^1.1.0",
23
23
  "@mui/icons-material": "^7.3.4",
24
24
  "@mui/material": "^7.3.4",
25
- "@pie-lib/drag": "^4.1.1",
26
- "@pie-lib/editable-html-tip-tap": "^2.1.14",
25
+ "@pie-lib/drag": "^4.1.2",
26
+ "@pie-lib/editable-html-tip-tap": "^2.1.15",
27
27
  "@pie-lib/graphing-utils": "^3.0.2",
28
- "@pie-lib/plot": "^4.0.16",
29
- "@pie-lib/render-ui": "^7.0.0",
28
+ "@pie-lib/plot": "^4.0.17",
29
+ "@pie-lib/render-ui": "^7.0.1",
30
30
  "@pie-lib/translator": "^4.0.2",
31
31
  "@visx/axis": "^3.0.0",
32
32
  "@visx/clip-path": "^3.0.0",
@@ -54,6 +54,6 @@
54
54
  "peerDependencies": {
55
55
  "react": "^18.2.0"
56
56
  },
57
- "gitHead": "e6b46157ab321f4488117149f21783511f30970c",
57
+ "gitHead": "371130452b7db8525d6f162c0c99495be08ced46",
58
58
  "scripts": {}
59
59
  }
@@ -765,6 +765,25 @@ describe('label focus', () => {
765
765
  expect(document.activeElement).toBe(input);
766
766
  });
767
767
 
768
+ // in the DNA env the focus is moved while the model save is handled, after the last render
769
+ it('takes the focus back when something steals it while the model is saved', () => {
770
+ const { container } = render(<Host />);
771
+
772
+ clickPoint(container);
773
+ const input = container.querySelector('foreignObject input');
774
+ expect(document.activeElement).toBe(input);
775
+
776
+ const elsewhere = document.createElement('input');
777
+ document.body.appendChild(elsewhere);
778
+ elsewhere.focus();
779
+ expect(document.activeElement).toBe(elsewhere);
780
+
781
+ act(() => jest.advanceTimersByTime(300));
782
+
783
+ expect(document.activeElement).toBe(input);
784
+ elsewhere.remove();
785
+ });
786
+
768
787
  it('focuses the label even if the tool was remounted before it rendered', () => {
769
788
  const { container } = render(<Host shiftMarks={true} />);
770
789
 
@@ -82,13 +82,13 @@ describe('MarkLabel - editing', () => {
82
82
  });
83
83
 
84
84
  it('does not focus the input when disabled', () => {
85
- const { input } = renderComponent({ autoFocus: true, disabled: true });
85
+ const { input } = renderComponent({ autoFocus: true, disabled: true, mark: { x: 1, y: 1, label: 'A' } });
86
86
 
87
87
  expect(document.activeElement).not.toBe(input);
88
88
  });
89
89
 
90
90
  it('does not focus the input of a disabled mark', () => {
91
- const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: '', disabled: true } });
91
+ const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'A', disabled: true } });
92
92
 
93
93
  expect(document.activeElement).not.toBe(input);
94
94
  });
@@ -191,10 +191,17 @@ describe('MarkLabel - editing', () => {
191
191
 
192
192
  // outside of label mode the input is disabled, which is how an empty label that was loaded with
193
193
  // the item is told apart from one the user has just added - label mode always starts off
194
- it('is not focused while label mode is off', () => {
195
- const { input } = renderComponent({ disabled: true, mark: { x: 1, y: 1, label: '' } });
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: '' } });
196
197
 
197
- expect(document.activeElement).not.toBe(input);
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');
198
205
  });
199
206
 
200
207
  it('is not focused when label mode is turned on afterwards', () => {
@@ -202,13 +209,29 @@ describe('MarkLabel - editing', () => {
202
209
 
203
210
  rerenderWith({ disabled: false });
204
211
 
212
+ expect(input(container)).not.toBe(null);
205
213
  expect(document.activeElement).not.toBe(input(container));
206
214
  });
207
215
 
208
- it('is not focused when the mark is disabled', () => {
209
- const { input } = renderComponent({ mark: { x: 1, y: 1, label: '', disabled: true } });
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 } });
210
218
 
211
- expect(document.activeElement).not.toBe(input);
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);
212
235
  });
213
236
  });
214
237
 
@@ -238,6 +261,75 @@ describe('MarkLabel - editing', () => {
238
261
  });
239
262
  });
240
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
+
241
333
  describe('onBlur', () => {
242
334
  it('is called so the tool stops auto focusing the label', () => {
243
335
  const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'A' } });
@@ -247,6 +339,29 @@ describe('MarkLabel - editing', () => {
247
339
  expect(onBlur).toHaveBeenCalled();
248
340
  });
249
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
+
250
365
  it('reports an empty label on blur', () => {
251
366
  const { input } = renderComponent({ mark: { x: 1, y: 1, label: 'A' } });
252
367
 
@@ -8,6 +8,16 @@ import { color } from '@pie-lib/render-ui';
8
8
  import SvgIcon from './label-svg-icon';
9
9
 
10
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;
11
21
 
12
22
  const StyledInputCorrect = styled('div')(({ theme }) => ({
13
23
  float: 'right',
@@ -105,7 +115,28 @@ export const coordinates = (graphProps, mark, rect = { width: 0, height: 0 }, po
105
115
  }
106
116
  };
107
117
 
108
- const LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, onChange, onBlur }) => (
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 }) => (
109
140
  <AutosizeInput
110
141
  inputRef={(r) => {
111
142
  _ref(r);
@@ -113,6 +144,7 @@ const LabelInput = ({ _ref, externalInputRef, label, disabled, inputStyle, onCha
113
144
  }}
114
145
  disabled={disabled}
115
146
  inputStyle={inputStyle}
147
+ minWidth={minWidth}
116
148
  value={label}
117
149
  onChange={onChange}
118
150
  onBlur={onBlur}
@@ -125,13 +157,13 @@ LabelInput.propTypes = {
125
157
  label: PropTypes.string,
126
158
  disabled: PropTypes.bool,
127
159
  inputStyle: PropTypes.object,
160
+ minWidth: PropTypes.number,
128
161
  onChange: PropTypes.func,
129
162
  onBlur: PropTypes.func,
130
163
  };
131
164
 
132
165
  export const MarkLabel = (props) => {
133
- // the node is kept in a ref as well as in the state: the state is needed to reposition the label
134
- // once it has a size, the ref is what the focus is asserted on - it is never a render behind
166
+ // the state repositions the label once the input has a size, the ref is never a render behind
135
167
  const inputNode = useRef(null);
136
168
  const [input, setInput] = useState(null);
137
169
  const _ref = useCallback((node) => {
@@ -148,33 +180,58 @@ export const MarkLabel = (props) => {
148
180
  const isFocused = () =>
149
181
  !!inputNode.current && typeof document !== 'undefined' && document.activeElement === inputNode.current;
150
182
 
151
- // A label that has just been added is empty and its input is enabled - label mode has to be on
152
- // for that to happen and it always starts off, so a mark that was loaded with an empty label
153
- // cannot grab the focus. This is read from the mark rather than taken from the autoFocus prop
154
- // because the tool that asked for the label can be remounted by the marks coming back from the
155
- // host (async model save) before the input ever renders, and its autoFocus goes with it.
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.
156
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);
157
197
 
158
198
  const onChange = (e) => setLabel(e.target.value);
159
199
 
200
+ const saveLabel = (value) => {
201
+ if (value === lastSaved.current) {
202
+ return;
203
+ }
204
+
205
+ lastSaved.current = value;
206
+ props.onChange(value);
207
+ };
208
+
160
209
  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
+
161
215
  isNewLabel.current = false;
162
216
 
163
217
  if (label === '') {
164
218
  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);
165
223
  }
166
224
 
167
225
  // lets the tool know that this label is not being edited anymore, so it stops auto focusing it
168
226
  if (props.onBlur) {
169
227
  props.onBlur();
170
228
  }
171
- }, [label, props.onChange, props.onBlur]);
229
+ }, [label, mark.label, props.onChange, props.onBlur]);
172
230
 
173
231
  const debouncedLabel = useDebounce(label, DEBOUNCE_DELAY);
174
232
 
175
- // useState only sets the value once, to synch props to state need useEffect.
176
- // While this input has the focus we keep what the user typed: the mark can come back from an
177
- // (async) model save with an older label and that would overwrite the characters typed meanwhile.
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
178
235
  useEffect(() => {
179
236
  if (isFocused()) {
180
237
  return;
@@ -186,38 +243,109 @@ export const MarkLabel = (props) => {
186
243
  // pick up the change to debouncedLabel and save it
187
244
  useEffect(() => {
188
245
  if (typeof debouncedLabel === 'string' && debouncedLabel !== mark.label) {
189
- props.onChange(debouncedLabel);
246
+ saveLabel(debouncedLabel);
190
247
  }
191
248
  }, [debouncedLabel]);
192
249
 
193
- // A label that has just been added has to be focused so that it can be typed into right away.
194
- // This runs after every render on purpose - there is no reliable single moment to focus at:
195
- // the input shows up whenever the mark comes back from the (async) model save, and a remount
196
- // of the input (same save, replaced node) drops the focus without this component being told.
197
- // Asserting the focus on every render is what makes it stick; it is a no-op once the input has
198
- // it, so the caret the user placed is left alone.
199
- useEffect(() => {
250
+ const focusInput = () => {
200
251
  const node = inputNode.current;
201
252
 
202
253
  // node.disabled, not the disabled prop: a disabled input silently ignores focus()
203
- if ((!autoFocus && !isNewLabel.current) || !node || node.disabled || isFocused()) {
254
+ if ((!autoFocus && !isNewLabel.current && !insisting()) || !node || node.disabled) {
204
255
  return;
205
256
  }
206
257
 
207
- node.focus();
258
+ const wasFocused = isFocused();
208
259
 
209
- // a new label is focused once, so it cannot fight over the focus with another one
210
- if (isFocused()) {
211
- isNewLabel.current = false;
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;
212
271
  }
213
272
 
214
- // keep the caret at the end, otherwise typing continues in front of the existing label
215
- const caret = (node.value || '').length;
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
+ }
216
286
 
217
- if (node.setSelectionRange) {
218
- node.setSelectionRange(caret, caret);
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;
219
292
  }
220
- });
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]);
221
349
 
222
350
  const rect = input ? input.getBoundingClientRect() : { width: 0, height: 0 };
223
351
  const pos = position(graphProps, mark, rect);
@@ -246,6 +374,11 @@ export const MarkLabel = (props) => {
246
374
  label={labelValue}
247
375
  disabled={disabledInput}
248
376
  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}
249
382
  onChange={onChange}
250
383
  onBlur={handleBlur}
251
384
  />
@@ -290,6 +423,12 @@ export const MarkLabel = (props) => {
290
423
  return <StyledIncorrect style={style}>{renderInput(studentInputStyle, label)}</StyledIncorrect>;
291
424
  }
292
425
 
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
+
293
432
  return <div style={style}>{renderInput(getInputStyles(theme, disabled, mark.disabled), label)}</div>;
294
433
  };
295
434
 
@@ -409,7 +409,8 @@ describe('Component', () => {
409
409
  expect(document.activeElement).not.toBe(labelNode.querySelector('input'));
410
410
  });
411
411
 
412
- it('stops auto focusing the label once it is blurred', () => {
412
+ // the blur that the DNA env causes by moving the focus is not the user leaving the label
413
+ it('keeps the focus on a new empty label that something else blurred', () => {
413
414
  const { container } = render(<Controlled initialMark={xy(1, 1)} labelModeEnabled={true} />);
414
415
 
415
416
  clickPoint(container);
@@ -417,9 +418,23 @@ describe('Component', () => {
417
418
  expect(document.activeElement).toBe(input);
418
419
 
419
420
  fireEvent.blur(input);
421
+ fireEvent.change(input, { target: { value: 'A' } });
422
+
423
+ expect(document.activeElement).toBe(input);
424
+ });
425
+
426
+ it('stops auto focusing the label once it has text and is blurred', () => {
427
+ const { container } = render(<Controlled initialMark={xy(1, 1)} labelModeEnabled={true} />);
428
+
429
+ clickPoint(container);
430
+ const input = labelNode.querySelector('input');
420
431
 
421
- // a re-render must not pull the focus back into the label the user just left
422
432
  fireEvent.change(input, { target: { value: 'A' } });
433
+ fireEvent.blur(input);
434
+ input.blur();
435
+
436
+ // a re-render must not pull the focus back into the label the user just left
437
+ fireEvent.change(input, { target: { value: 'AB' } });
423
438
  expect(document.activeElement).not.toBe(input);
424
439
  });
425
440