@pie-lib/graphing 4.0.15 → 4.0.16
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 +6 -0
- package/lib/mark-label.js +36 -3
- package/lib/mark-label.js.map +1 -1
- package/lib/tools/circle/component.js +34 -3
- package/lib/tools/circle/component.js.map +1 -1
- package/lib/tools/point/component.js +29 -8
- package/lib/tools/point/component.js.map +1 -1
- package/lib/tools/polygon/component.js +31 -6
- package/lib/tools/polygon/component.js.map +1 -1
- package/lib/tools/shared/line/index.js +34 -3
- package/lib/tools/shared/line/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/mark-label.test.jsx +166 -1
- package/src/mark-label.jsx +39 -4
- package/src/tools/circle/component.jsx +26 -3
- package/src/tools/point/__tests__/component.test.jsx +112 -1
- package/src/tools/point/component.jsx +21 -8
- package/src/tools/polygon/component.jsx +24 -6
- package/src/tools/shared/line/index.jsx +26 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render } from '@pie-lib/test-utils';
|
|
1
|
+
import { act, fireEvent, 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,6 +30,171 @@ 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 when autoFocus is not set', () => {
|
|
79
|
+
const { input } = renderComponent();
|
|
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 });
|
|
86
|
+
|
|
87
|
+
expect(document.activeElement).not.toBe(input);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('focuses the input once it is no longer disabled', () => {
|
|
91
|
+
const { rerenderWith, container } = renderComponent({ autoFocus: true, disabled: true });
|
|
92
|
+
|
|
93
|
+
rerenderWith({ disabled: false });
|
|
94
|
+
|
|
95
|
+
expect(document.activeElement).toBe(input(container));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// the host saves the model with an api call and the model that comes back can replace the
|
|
99
|
+
// input node - the label has to keep the focus across that remount
|
|
100
|
+
it('re-focuses the input when it is remounted while being edited', () => {
|
|
101
|
+
const props = {
|
|
102
|
+
autoFocus: true,
|
|
103
|
+
onChange,
|
|
104
|
+
inputRef: jest.fn(),
|
|
105
|
+
mark: { x: 1, y: 1, label: 'A' },
|
|
106
|
+
graphProps: getGraphProps(0, 10, 0, 10),
|
|
107
|
+
};
|
|
108
|
+
const { container, rerender } = render(<MarkLabel key="first" {...props} />);
|
|
109
|
+
const first = input(container);
|
|
110
|
+
|
|
111
|
+
expect(document.activeElement).toBe(first);
|
|
112
|
+
|
|
113
|
+
rerender(<MarkLabel key="second" {...props} />);
|
|
114
|
+
const second = input(container);
|
|
115
|
+
|
|
116
|
+
expect(second).not.toBe(first);
|
|
117
|
+
expect(document.activeElement).toBe(second);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('syncing mark.label into the input', () => {
|
|
122
|
+
it('picks up a new mark.label while the input does not have the focus', () => {
|
|
123
|
+
const { rerenderWith, container } = renderComponent({ mark: { x: 1, y: 1, label: 'A' } });
|
|
124
|
+
|
|
125
|
+
rerenderWith({ mark: { x: 1, y: 1, label: 'updated' } });
|
|
126
|
+
|
|
127
|
+
expect(input(container).value).toBe('updated');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// a mark coming back from an (async) model save can carry an older label - applying it would
|
|
131
|
+
// wipe out the characters typed in the meantime
|
|
132
|
+
it('keeps what the user typed while the input has the focus', () => {
|
|
133
|
+
const { input, rerenderWith } = renderComponent({ autoFocus: true });
|
|
134
|
+
|
|
135
|
+
expect(document.activeElement).toBe(input);
|
|
136
|
+
|
|
137
|
+
fireEvent.change(input, { target: { value: 'AB' } });
|
|
138
|
+
expect(input.value).toBe('AB');
|
|
139
|
+
|
|
140
|
+
// the host echoes back the label as it was one keystroke ago
|
|
141
|
+
rerenderWith({ mark: { x: 1, y: 1, label: 'A' } });
|
|
142
|
+
|
|
143
|
+
expect(input.value).toBe('AB');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('onBlur', () => {
|
|
148
|
+
it('is called so the tool stops auto focusing the label', () => {
|
|
149
|
+
const { input } = renderComponent({ autoFocus: true, mark: { x: 1, y: 1, label: 'A' } });
|
|
150
|
+
|
|
151
|
+
fireEvent.blur(input);
|
|
152
|
+
|
|
153
|
+
expect(onBlur).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports an empty label on blur', () => {
|
|
157
|
+
const { input } = renderComponent({ mark: { x: 1, y: 1, label: 'A' } });
|
|
158
|
+
|
|
159
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
160
|
+
fireEvent.blur(input);
|
|
161
|
+
|
|
162
|
+
expect(onChange).toHaveBeenCalledWith('');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('debounce', () => {
|
|
167
|
+
beforeEach(() => jest.useFakeTimers());
|
|
168
|
+
afterEach(() => jest.useRealTimers());
|
|
169
|
+
|
|
170
|
+
it('does not report the typed label before the debounce delay elapses', () => {
|
|
171
|
+
const { input } = renderComponent({ autoFocus: true });
|
|
172
|
+
|
|
173
|
+
fireEvent.change(input, { target: { value: 'A' } });
|
|
174
|
+
|
|
175
|
+
act(() => jest.advanceTimersByTime(499));
|
|
176
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
177
|
+
|
|
178
|
+
act(() => jest.advanceTimersByTime(1));
|
|
179
|
+
expect(onChange).toHaveBeenCalledWith('A');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('reports the label once for a burst of keystrokes', () => {
|
|
183
|
+
const { input } = renderComponent({ autoFocus: true });
|
|
184
|
+
|
|
185
|
+
fireEvent.change(input, { target: { value: 'A' } });
|
|
186
|
+
act(() => jest.advanceTimersByTime(300));
|
|
187
|
+
fireEvent.change(input, { target: { value: 'AB' } });
|
|
188
|
+
act(() => jest.advanceTimersByTime(300));
|
|
189
|
+
fireEvent.change(input, { target: { value: 'ABC' } });
|
|
190
|
+
act(() => jest.advanceTimersByTime(500));
|
|
191
|
+
|
|
192
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(onChange).toHaveBeenCalledWith('ABC');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
33
198
|
describe('position', () => {
|
|
34
199
|
const assertPosition = (mark, rect, expected) => {
|
|
35
200
|
it(`${mark.x},${mark.y} + ${rect.width},${rect.height} => ${expected}`, () => {
|
package/src/mark-label.jsx
CHANGED
|
@@ -7,6 +7,8 @@ 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
|
+
|
|
10
12
|
const StyledInputCorrect = styled('div')(({ theme }) => ({
|
|
11
13
|
float: 'right',
|
|
12
14
|
padding: theme.spacing(0.5),
|
|
@@ -132,23 +134,36 @@ export const MarkLabel = (props) => {
|
|
|
132
134
|
const _ref = useCallback((node) => setInput(node));
|
|
133
135
|
const theme = useTheme();
|
|
134
136
|
|
|
135
|
-
const { mark, graphProps, disabled, inputRef: externalInputRef } = props;
|
|
137
|
+
const { mark, graphProps, disabled, autoFocus, inputRef: externalInputRef } = props;
|
|
136
138
|
|
|
137
139
|
const [label, setLabel] = useState(mark.label);
|
|
138
140
|
const { correctness, correctnesslabel, correctlabel } = mark;
|
|
139
141
|
|
|
142
|
+
const isFocused = () => !!input && typeof document !== 'undefined' && document.activeElement === input;
|
|
143
|
+
|
|
140
144
|
const onChange = (e) => setLabel(e.target.value);
|
|
141
145
|
|
|
142
146
|
const handleBlur = useCallback(() => {
|
|
143
147
|
if (label === '') {
|
|
144
148
|
props.onChange('');
|
|
145
149
|
}
|
|
146
|
-
}, [label, props.onChange]);
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
// lets the tool know that this label is not being edited anymore, so it stops auto focusing it
|
|
152
|
+
if (props.onBlur) {
|
|
153
|
+
props.onBlur();
|
|
154
|
+
}
|
|
155
|
+
}, [label, props.onChange, props.onBlur]);
|
|
156
|
+
|
|
157
|
+
const debouncedLabel = useDebounce(label, DEBOUNCE_DELAY);
|
|
149
158
|
|
|
150
|
-
// useState only sets the value once, to synch props to state need useEffect
|
|
159
|
+
// useState only sets the value once, to synch props to state need useEffect.
|
|
160
|
+
// While this input has the focus we keep what the user typed: the mark can come back from an
|
|
161
|
+
// (async) model save with an older label and that would overwrite the characters typed meanwhile.
|
|
151
162
|
useEffect(() => {
|
|
163
|
+
if (isFocused()) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
setLabel(mark.label);
|
|
153
168
|
}, [mark.label]);
|
|
154
169
|
|
|
@@ -159,6 +174,24 @@ export const MarkLabel = (props) => {
|
|
|
159
174
|
}
|
|
160
175
|
}, [debouncedLabel]);
|
|
161
176
|
|
|
177
|
+
// A label that has just been added has to be focused so that it can be typed into right away.
|
|
178
|
+
// This also re-focuses the input if it gets remounted while it is being edited - saving the model
|
|
179
|
+
// is done by the host (api call) and the model that comes back can replace the input node.
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!autoFocus || !input || disabled || isFocused()) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
input.focus();
|
|
186
|
+
|
|
187
|
+
// keep the caret at the end, otherwise typing continues in front of the existing label
|
|
188
|
+
const caret = (input.value || '').length;
|
|
189
|
+
|
|
190
|
+
if (input.setSelectionRange) {
|
|
191
|
+
input.setSelectionRange(caret, caret);
|
|
192
|
+
}
|
|
193
|
+
}, [autoFocus, input, disabled]);
|
|
194
|
+
|
|
162
195
|
const rect = input ? input.getBoundingClientRect() : { width: 0, height: 0 };
|
|
163
196
|
const pos = position(graphProps, mark, rect);
|
|
164
197
|
const leftTop = coordinates(graphProps, mark, rect, pos);
|
|
@@ -234,7 +267,9 @@ export const MarkLabel = (props) => {
|
|
|
234
267
|
};
|
|
235
268
|
|
|
236
269
|
MarkLabel.propTypes = {
|
|
270
|
+
autoFocus: PropTypes.bool,
|
|
237
271
|
disabled: PropTypes.bool,
|
|
272
|
+
onBlur: PropTypes.func,
|
|
238
273
|
onChange: PropTypes.func,
|
|
239
274
|
graphProps: types.GraphPropsType,
|
|
240
275
|
inputRef: PropTypes.func,
|
|
@@ -49,6 +49,16 @@ export class RawBaseCircle extends React.Component {
|
|
|
49
49
|
onClick: () => ({}),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
// which of from/to/middle has its label edited, null if none
|
|
53
|
+
state = { editingLabel: null };
|
|
54
|
+
|
|
55
|
+
componentDidUpdate(prevProps) {
|
|
56
|
+
// leaving label mode closes any label that is still being edited
|
|
57
|
+
if (prevProps.labelModeEnabled && !this.props.labelModeEnabled && this.state.editingLabel !== null) {
|
|
58
|
+
this.setState({ editingLabel: null });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
onChangePoint = (point) => {
|
|
53
63
|
const { middle, onChange } = this.props;
|
|
54
64
|
const { from, to } = point;
|
|
@@ -102,6 +112,12 @@ export class RawBaseCircle extends React.Component {
|
|
|
102
112
|
changeMarkProps({ [type]: update });
|
|
103
113
|
};
|
|
104
114
|
|
|
115
|
+
stopEditingLabel = () => {
|
|
116
|
+
if (this.state.editingLabel !== null) {
|
|
117
|
+
this.setState({ editingLabel: null });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
105
121
|
clickPoint = (point, type, data) => {
|
|
106
122
|
const { changeMarkProps, disabled, from, to, middle, labelModeEnabled, limitLabeling, onClick } = this.props;
|
|
107
123
|
|
|
@@ -122,9 +138,9 @@ export class RawBaseCircle extends React.Component {
|
|
|
122
138
|
[type]: { label: '', ...point },
|
|
123
139
|
});
|
|
124
140
|
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
}
|
|
141
|
+
// MarkLabel focuses itself once it is rendered - it also holds on to the focus if the model
|
|
142
|
+
// that comes back from the host (api save) remounts the input.
|
|
143
|
+
this.setState({ editingLabel: type });
|
|
128
144
|
};
|
|
129
145
|
|
|
130
146
|
input = {};
|
|
@@ -145,6 +161,7 @@ export class RawBaseCircle extends React.Component {
|
|
|
145
161
|
labelNode,
|
|
146
162
|
labelModeEnabled,
|
|
147
163
|
} = this.props;
|
|
164
|
+
const { editingLabel } = this.state;
|
|
148
165
|
|
|
149
166
|
const common = { onDragStart, onDragStop, graphProps, onClick };
|
|
150
167
|
to = to || from;
|
|
@@ -159,9 +176,11 @@ export class RawBaseCircle extends React.Component {
|
|
|
159
176
|
fromLabelNode = ReactDOM.createPortal(
|
|
160
177
|
<MarkLabel
|
|
161
178
|
inputRef={(r) => (this.input.from = r)}
|
|
179
|
+
autoFocus={editingLabel === 'from'}
|
|
162
180
|
disabled={!labelModeEnabled}
|
|
163
181
|
mark={from}
|
|
164
182
|
graphProps={graphProps}
|
|
183
|
+
onBlur={this.stopEditingLabel}
|
|
165
184
|
onChange={(label) => this.labelChange({ ...from, label }, 'from')}
|
|
166
185
|
/>,
|
|
167
186
|
labelNode,
|
|
@@ -172,9 +191,11 @@ export class RawBaseCircle extends React.Component {
|
|
|
172
191
|
toLabelNode = ReactDOM.createPortal(
|
|
173
192
|
<MarkLabel
|
|
174
193
|
inputRef={(r) => (this.input.to = r)}
|
|
194
|
+
autoFocus={editingLabel === 'to'}
|
|
175
195
|
disabled={!labelModeEnabled}
|
|
176
196
|
mark={to}
|
|
177
197
|
graphProps={graphProps}
|
|
198
|
+
onBlur={this.stopEditingLabel}
|
|
178
199
|
onChange={(label) => this.labelChange({ ...to, label }, 'to')}
|
|
179
200
|
/>,
|
|
180
201
|
labelNode,
|
|
@@ -185,9 +206,11 @@ export class RawBaseCircle extends React.Component {
|
|
|
185
206
|
circleLabelNode = ReactDOM.createPortal(
|
|
186
207
|
<MarkLabel
|
|
187
208
|
inputRef={(r) => (this.input.middle = r)}
|
|
209
|
+
autoFocus={editingLabel === 'middle'}
|
|
188
210
|
disabled={!labelModeEnabled}
|
|
189
211
|
mark={middle}
|
|
190
212
|
graphProps={graphProps}
|
|
213
|
+
onBlur={this.stopEditingLabel}
|
|
191
214
|
onChange={(label) => this.labelChange({ ...middle, label }, 'middle')}
|
|
192
215
|
/>,
|
|
193
216
|
labelNode,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render } from '@pie-lib/test-utils';
|
|
1
|
+
import { fireEvent, render } from '@pie-lib/test-utils';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { graphProps, xy } from '../../../__tests__/utils';
|
|
4
4
|
|
|
@@ -346,4 +346,115 @@ describe('Component', () => {
|
|
|
346
346
|
expect(container.firstChild).toBeInTheDocument();
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
describe('label focus', () => {
|
|
351
|
+
let labelNode;
|
|
352
|
+
|
|
353
|
+
// the label is rendered into a portal - it has to be in the document for it to take the focus
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
labelNode = document.createElement('div');
|
|
356
|
+
document.body.appendChild(labelNode);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
afterEach(() => {
|
|
360
|
+
document.body.removeChild(labelNode);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// the mark is owned by the container, so the tool only sees a new label once it is handed back
|
|
364
|
+
const Controlled = ({ initialMark, labelModeEnabled }) => {
|
|
365
|
+
const [mark, setMark] = React.useState(initialMark);
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<Component
|
|
369
|
+
mark={mark}
|
|
370
|
+
onChange={(current, update) => setMark(update)}
|
|
371
|
+
onClick={onClick}
|
|
372
|
+
graphProps={graphProps()}
|
|
373
|
+
labelNode={labelNode}
|
|
374
|
+
labelModeEnabled={labelModeEnabled}
|
|
375
|
+
/>
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const clickPoint = (container) => fireEvent.click(container.querySelector('circle'));
|
|
380
|
+
|
|
381
|
+
it('focuses the new label when a point is clicked in label mode', () => {
|
|
382
|
+
const { container } = render(<Controlled initialMark={xy(1, 1)} labelModeEnabled={true} />);
|
|
383
|
+
|
|
384
|
+
expect(labelNode.querySelector('input')).toBe(null);
|
|
385
|
+
|
|
386
|
+
clickPoint(container);
|
|
387
|
+
|
|
388
|
+
const input = labelNode.querySelector('input');
|
|
389
|
+
expect(input).not.toBe(null);
|
|
390
|
+
expect(document.activeElement).toBe(input);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('does not focus the label when a point is clicked outside of label mode', () => {
|
|
394
|
+
const { container } = render(<Controlled initialMark={{ ...xy(1, 1), label: 'A' }} labelModeEnabled={false} />);
|
|
395
|
+
|
|
396
|
+
clickPoint(container);
|
|
397
|
+
|
|
398
|
+
expect(onClick).toHaveBeenCalled();
|
|
399
|
+
expect(document.activeElement).not.toBe(labelNode.querySelector('input'));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('does not focus the label of a disabled mark', () => {
|
|
403
|
+
const { container } = render(
|
|
404
|
+
<Controlled initialMark={{ ...xy(1, 1), label: 'A', disabled: true }} labelModeEnabled={true} />,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
clickPoint(container);
|
|
408
|
+
|
|
409
|
+
expect(document.activeElement).not.toBe(labelNode.querySelector('input'));
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('stops auto focusing the label once it is blurred', () => {
|
|
413
|
+
const { container } = render(<Controlled initialMark={xy(1, 1)} labelModeEnabled={true} />);
|
|
414
|
+
|
|
415
|
+
clickPoint(container);
|
|
416
|
+
const input = labelNode.querySelector('input');
|
|
417
|
+
expect(document.activeElement).toBe(input);
|
|
418
|
+
|
|
419
|
+
fireEvent.blur(input);
|
|
420
|
+
|
|
421
|
+
// a re-render must not pull the focus back into the label the user just left
|
|
422
|
+
fireEvent.change(input, { target: { value: 'A' } });
|
|
423
|
+
expect(document.activeElement).not.toBe(input);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('closes the label that is being edited when label mode is left', () => {
|
|
427
|
+
const ref = React.createRef();
|
|
428
|
+
const { container, rerender } = render(
|
|
429
|
+
<Component
|
|
430
|
+
ref={ref}
|
|
431
|
+
mark={xy(1, 1)}
|
|
432
|
+
onChange={onChange}
|
|
433
|
+
onClick={onClick}
|
|
434
|
+
graphProps={graphProps()}
|
|
435
|
+
labelNode={labelNode}
|
|
436
|
+
labelModeEnabled={true}
|
|
437
|
+
/>,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
clickPoint(container);
|
|
441
|
+
expect(ref.current.state.editingLabel).toBe('label');
|
|
442
|
+
|
|
443
|
+
rerender(
|
|
444
|
+
<Component
|
|
445
|
+
ref={ref}
|
|
446
|
+
mark={xy(1, 1)}
|
|
447
|
+
onChange={onChange}
|
|
448
|
+
onClick={onClick}
|
|
449
|
+
graphProps={graphProps()}
|
|
450
|
+
labelNode={labelNode}
|
|
451
|
+
labelModeEnabled={false}
|
|
452
|
+
/>,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// there is no user visible path for this one: a real blur resets it through onBlur, this is
|
|
456
|
+
// the safety net for a label that is still marked as edited when label mode goes away
|
|
457
|
+
expect(ref.current.state.editingLabel).toBe(null);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
349
460
|
});
|
|
@@ -16,7 +16,15 @@ export class Point extends React.Component {
|
|
|
16
16
|
|
|
17
17
|
constructor(props) {
|
|
18
18
|
super(props);
|
|
19
|
-
|
|
19
|
+
// which label is being edited, null if none - a point only has the one
|
|
20
|
+
this.state = { editingLabel: null };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
componentDidUpdate(prevProps) {
|
|
24
|
+
// leaving label mode closes any label that is still being edited
|
|
25
|
+
if (prevProps.labelModeEnabled && !this.props.labelModeEnabled && this.state.editingLabel !== null) {
|
|
26
|
+
this.setState({ editingLabel: null });
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
move = (p) => {
|
|
@@ -59,6 +67,12 @@ export class Point extends React.Component {
|
|
|
59
67
|
});
|
|
60
68
|
};
|
|
61
69
|
|
|
70
|
+
stopEditingLabel = () => {
|
|
71
|
+
if (this.state.editingLabel !== null) {
|
|
72
|
+
this.setState({ editingLabel: null });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
62
76
|
clickPoint = () => {
|
|
63
77
|
const { labelModeEnabled, onChange, onClick, mark } = this.props;
|
|
64
78
|
|
|
@@ -73,17 +87,14 @@ export class Point extends React.Component {
|
|
|
73
87
|
|
|
74
88
|
onChange(mark, { label: '', ...mark });
|
|
75
89
|
|
|
76
|
-
// MarkLabel
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
if (this.input) {
|
|
80
|
-
this.input.focus();
|
|
81
|
-
}
|
|
82
|
-
}, 0);
|
|
90
|
+
// MarkLabel focuses itself once it is rendered - it also holds on to the focus if the model
|
|
91
|
+
// that comes back from the host (api save) remounts the input.
|
|
92
|
+
this.setState({ editingLabel: 'label' });
|
|
83
93
|
};
|
|
84
94
|
|
|
85
95
|
render() {
|
|
86
96
|
const { coordinatesOnHover, graphProps, labelNode, labelModeEnabled } = this.props;
|
|
97
|
+
const { editingLabel } = this.state;
|
|
87
98
|
const mark = this.state.mark ? this.state.mark : this.props.mark;
|
|
88
99
|
|
|
89
100
|
return (
|
|
@@ -111,9 +122,11 @@ export class Point extends React.Component {
|
|
|
111
122
|
ReactDOM.createPortal(
|
|
112
123
|
<MarkLabel
|
|
113
124
|
inputRef={(r) => (this.input = r)}
|
|
125
|
+
autoFocus={editingLabel === 'label'}
|
|
114
126
|
disabled={!labelModeEnabled}
|
|
115
127
|
mark={mark}
|
|
116
128
|
graphProps={graphProps}
|
|
129
|
+
onBlur={this.stopEditingLabel}
|
|
117
130
|
onChange={this.labelChange}
|
|
118
131
|
/>,
|
|
119
132
|
labelNode,
|
|
@@ -90,6 +90,16 @@ export class RawBaseComponent extends React.Component {
|
|
|
90
90
|
points: [],
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
// index of the point whose label is edited (points.length for the polygon label), null if none
|
|
94
|
+
state = { editingLabel: null };
|
|
95
|
+
|
|
96
|
+
componentDidUpdate(prevProps) {
|
|
97
|
+
// leaving label mode closes any label that is still being edited
|
|
98
|
+
if (prevProps.labelModeEnabled && !this.props.labelModeEnabled && this.state.editingLabel !== null) {
|
|
99
|
+
this.setState({ editingLabel: null });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
dragPoint = (index, from, to) => {
|
|
94
104
|
log('[dragPoint] from, to:', from, to);
|
|
95
105
|
const { onChange, points } = this.props;
|
|
@@ -162,6 +172,12 @@ export class RawBaseComponent extends React.Component {
|
|
|
162
172
|
onChangeProps(update);
|
|
163
173
|
};
|
|
164
174
|
|
|
175
|
+
stopEditingLabel = () => {
|
|
176
|
+
if (this.state.editingLabel !== null) {
|
|
177
|
+
this.setState({ editingLabel: null });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
165
181
|
clickPoint = (point, index, data) => {
|
|
166
182
|
const {
|
|
167
183
|
closed,
|
|
@@ -194,12 +210,9 @@ export class RawBaseComponent extends React.Component {
|
|
|
194
210
|
onSetMiddleLabel(strippedMiddle, strippedPoints);
|
|
195
211
|
}
|
|
196
212
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.input[index].focus();
|
|
201
|
-
}
|
|
202
|
-
}, 0);
|
|
213
|
+
// MarkLabel focuses itself once it is rendered - it also holds on to the focus if the model
|
|
214
|
+
// that comes back from the host (api save) remounts the input.
|
|
215
|
+
this.setState({ editingLabel: index });
|
|
203
216
|
|
|
204
217
|
return;
|
|
205
218
|
}
|
|
@@ -230,6 +243,7 @@ export class RawBaseComponent extends React.Component {
|
|
|
230
243
|
labelNode,
|
|
231
244
|
labelModeEnabled,
|
|
232
245
|
} = this.props;
|
|
246
|
+
const { editingLabel } = this.state;
|
|
233
247
|
const lines = buildLines(points, closed);
|
|
234
248
|
const common = { onDragStart, onDragStop, graphProps, disabled, correctness };
|
|
235
249
|
const polygonLabelIndex = (points && points.length) || 0;
|
|
@@ -239,9 +253,11 @@ export class RawBaseComponent extends React.Component {
|
|
|
239
253
|
polygonLabelNode = ReactDOM.createPortal(
|
|
240
254
|
<MarkLabel
|
|
241
255
|
inputRef={(r) => (this.input[polygonLabelIndex] = r)}
|
|
256
|
+
autoFocus={editingLabel === polygonLabelIndex}
|
|
242
257
|
disabled={disabled || !labelModeEnabled}
|
|
243
258
|
mark={middle}
|
|
244
259
|
graphProps={graphProps}
|
|
260
|
+
onBlur={this.stopEditingLabel}
|
|
245
261
|
onChange={(label) => onChangeLabelProps({ ...middle, label })}
|
|
246
262
|
/>,
|
|
247
263
|
labelNode,
|
|
@@ -291,9 +307,11 @@ export class RawBaseComponent extends React.Component {
|
|
|
291
307
|
? ReactDOM.createPortal(
|
|
292
308
|
<MarkLabel
|
|
293
309
|
inputRef={(r) => (this.input[index] = r)}
|
|
310
|
+
autoFocus={editingLabel === index}
|
|
294
311
|
disabled={disabled || !labelModeEnabled}
|
|
295
312
|
mark={p}
|
|
296
313
|
graphProps={graphProps}
|
|
314
|
+
onBlur={this.stopEditingLabel}
|
|
297
315
|
onChange={(label) => this.labelChange({ ...p, label }, index)}
|
|
298
316
|
/>,
|
|
299
317
|
labelNode,
|
|
@@ -234,6 +234,16 @@ export const lineBase = (Comp, opts) => {
|
|
|
234
234
|
labelNode: PropTypes.object,
|
|
235
235
|
};
|
|
236
236
|
|
|
237
|
+
// which of from/to/middle has its label edited, null if none
|
|
238
|
+
state = { editingLabel: null };
|
|
239
|
+
|
|
240
|
+
componentDidUpdate(prevProps) {
|
|
241
|
+
// leaving label mode closes any label that is still being edited
|
|
242
|
+
if (prevProps.labelModeEnabled && !this.props.labelModeEnabled && this.state.editingLabel !== null) {
|
|
243
|
+
this.setState({ editingLabel: null });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
onChangePoint = (point) => {
|
|
238
248
|
const { middle, onChange } = this.props;
|
|
239
249
|
const { from, to } = point;
|
|
@@ -303,6 +313,12 @@ export const lineBase = (Comp, opts) => {
|
|
|
303
313
|
changeMarkProps({ [type]: update });
|
|
304
314
|
};
|
|
305
315
|
|
|
316
|
+
stopEditingLabel = () => {
|
|
317
|
+
if (this.state.editingLabel !== null) {
|
|
318
|
+
this.setState({ editingLabel: null });
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
306
322
|
clickPoint = (point, type, data) => {
|
|
307
323
|
const { changeMarkProps, disabled, from, to, middle, labelModeEnabled, limitLabeling, onClick } = this.props;
|
|
308
324
|
|
|
@@ -331,9 +347,9 @@ export const lineBase = (Comp, opts) => {
|
|
|
331
347
|
[type]: { label: '', ...point },
|
|
332
348
|
});
|
|
333
349
|
|
|
334
|
-
if
|
|
335
|
-
|
|
336
|
-
}
|
|
350
|
+
// MarkLabel focuses itself once it is rendered - it also holds on to the focus if the model
|
|
351
|
+
// that comes back from the host (api save) remounts the input.
|
|
352
|
+
this.setState({ editingLabel: type });
|
|
337
353
|
};
|
|
338
354
|
|
|
339
355
|
// IMPORTANT, do not remove
|
|
@@ -354,6 +370,7 @@ export const lineBase = (Comp, opts) => {
|
|
|
354
370
|
labelNode,
|
|
355
371
|
labelModeEnabled,
|
|
356
372
|
} = this.props;
|
|
373
|
+
const { editingLabel } = this.state;
|
|
357
374
|
const common = { graphProps, onDragStart, onDragStop, disabled, correctness, onClick };
|
|
358
375
|
const angle = to ? trig.toDegrees(trig.angle(from, to)) : 0;
|
|
359
376
|
|
|
@@ -366,9 +383,11 @@ export const lineBase = (Comp, opts) => {
|
|
|
366
383
|
fromLabelNode = ReactDOM.createPortal(
|
|
367
384
|
<MarkLabel
|
|
368
385
|
inputRef={(r) => (this.input.from = r)}
|
|
386
|
+
autoFocus={editingLabel === 'from'}
|
|
369
387
|
disabled={!labelModeEnabled}
|
|
370
388
|
mark={from}
|
|
371
389
|
graphProps={graphProps}
|
|
390
|
+
onBlur={this.stopEditingLabel}
|
|
372
391
|
onChange={(label) => this.labelChange({ ...from, label }, 'from')}
|
|
373
392
|
/>,
|
|
374
393
|
labelNode,
|
|
@@ -379,9 +398,11 @@ export const lineBase = (Comp, opts) => {
|
|
|
379
398
|
toLabelNode = ReactDOM.createPortal(
|
|
380
399
|
<MarkLabel
|
|
381
400
|
inputRef={(r) => (this.input.to = r)}
|
|
401
|
+
autoFocus={editingLabel === 'to'}
|
|
382
402
|
disabled={!labelModeEnabled}
|
|
383
403
|
mark={to}
|
|
384
404
|
graphProps={graphProps}
|
|
405
|
+
onBlur={this.stopEditingLabel}
|
|
385
406
|
onChange={(label) => this.labelChange({ ...to, label }, 'to')}
|
|
386
407
|
/>,
|
|
387
408
|
labelNode,
|
|
@@ -392,9 +413,11 @@ export const lineBase = (Comp, opts) => {
|
|
|
392
413
|
lineLabelNode = ReactDOM.createPortal(
|
|
393
414
|
<MarkLabel
|
|
394
415
|
inputRef={(r) => (this.input.middle = r)}
|
|
416
|
+
autoFocus={editingLabel === 'middle'}
|
|
395
417
|
disabled={!labelModeEnabled}
|
|
396
418
|
mark={middle}
|
|
397
419
|
graphProps={graphProps}
|
|
420
|
+
onBlur={this.stopEditingLabel}
|
|
398
421
|
onChange={(label) => this.labelChange({ ...middle, label }, 'middle')}
|
|
399
422
|
/>,
|
|
400
423
|
labelNode,
|