@pie-lib/mask-markup 3.1.5 → 3.1.6-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -105
- package/lib/__tests__/drag-in-the-blank.test.js +129 -0
- package/lib/__tests__/index.test.js +39 -0
- package/lib/__tests__/keyboard-coordinates.test.js +449 -0
- package/lib/__tests__/mask.test.js +344 -0
- package/lib/__tests__/serialization.test.js +44 -0
- package/lib/__tests__/utils.js +14 -0
- package/lib/__tests__/with-mask.test.js +110 -0
- package/lib/choices/__tests__/index.test.js +139 -0
- package/lib/choices/choice.js +6 -24
- package/lib/choices/choice.js.map +1 -1
- package/lib/choices/index.js +9 -32
- package/lib/choices/index.js.map +1 -1
- package/lib/components/__tests__/blank.test.js +293 -0
- package/lib/components/__tests__/correct-input.test.js +132 -0
- package/lib/components/__tests__/dropdown.test.js +202 -0
- package/lib/components/__tests__/input.test.js +129 -0
- package/lib/components/blank.js +29 -106
- package/lib/components/blank.js.map +1 -1
- package/lib/components/dropdown.js +9 -6
- package/lib/components/dropdown.js.map +1 -1
- package/lib/drag-in-the-blank.js +30 -139
- package/lib/drag-in-the-blank.js.map +1 -1
- package/lib/keyboard-coordinates.js +1 -1
- package/lib/mask.js +15 -20
- package/lib/mask.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/drag-in-the-blank.test.js +2 -147
- package/src/__tests__/mask.test.js +0 -98
- package/src/choices/__tests__/index.test.js +1 -104
- package/src/choices/choice.jsx +3 -16
- package/src/choices/index.jsx +3 -27
- package/src/components/__tests__/blank.test.js +5 -186
- package/src/components/blank.jsx +35 -108
- package/src/components/dropdown.jsx +9 -7
- package/src/drag-in-the-blank.jsx +22 -132
- package/src/mask.jsx +11 -13
- package/LICENSE.md +0 -5
- package/lib/keyboard-coordinates.js.map +0 -1
- package/src/__tests__/keyboard-coordinates.test.js +0 -202
- package/src/keyboard-coordinates.js +0 -147
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { render, screen
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import Choice from '../choice';
|
|
4
4
|
import { choice } from '../../__tests__/utils';
|
|
5
5
|
import Choices from '../index';
|
|
@@ -65,48 +65,6 @@ describe('index', () => {
|
|
|
65
65
|
const { container } = render(<Choices {...defaultProps} duplicates={true} value={{ 0: '0', 1: '1' }} />);
|
|
66
66
|
expect(container.firstChild).toBeInTheDocument();
|
|
67
67
|
});
|
|
68
|
-
|
|
69
|
-
describe('click-to-place into the pool', () => {
|
|
70
|
-
it('places the selection into the pool when clicking the pool background', () => {
|
|
71
|
-
const onPlacementClick = jest.fn();
|
|
72
|
-
const selectedItem = { id: 'some-blank', fromChoice: false, type: 'MaskBlank' };
|
|
73
|
-
const { container } = render(
|
|
74
|
-
<Choices {...defaultProps} selectedItem={selectedItem} onPlacementClick={onPlacementClick} />,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
fireEvent.click(container.firstChild);
|
|
78
|
-
|
|
79
|
-
expect(onPlacementClick).toHaveBeenCalledWith(undefined);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('does nothing when clicking the pool background with nothing selected', () => {
|
|
83
|
-
const onPlacementClick = jest.fn();
|
|
84
|
-
const { container } = render(<Choices {...defaultProps} onPlacementClick={onPlacementClick} />);
|
|
85
|
-
|
|
86
|
-
fireEvent.click(container.firstChild);
|
|
87
|
-
|
|
88
|
-
expect(onPlacementClick).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('clicking a specific choice selects it, not the pool background handler', () => {
|
|
92
|
-
const onPlacementClick = jest.fn();
|
|
93
|
-
const onSelectClick = jest.fn();
|
|
94
|
-
const selectedItem = { id: 'some-blank', fromChoice: false, type: 'MaskBlank' };
|
|
95
|
-
render(
|
|
96
|
-
<Choices
|
|
97
|
-
{...defaultProps}
|
|
98
|
-
selectedItem={selectedItem}
|
|
99
|
-
onSelectClick={onSelectClick}
|
|
100
|
-
onPlacementClick={onPlacementClick}
|
|
101
|
-
/>,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
fireEvent.click(screen.getByText('Jumped'));
|
|
105
|
-
|
|
106
|
-
expect(onSelectClick).toHaveBeenCalled();
|
|
107
|
-
expect(onPlacementClick).not.toHaveBeenCalled();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
68
|
});
|
|
111
69
|
|
|
112
70
|
describe('Choice', () => {
|
|
@@ -147,66 +105,5 @@ describe('index', () => {
|
|
|
147
105
|
expect(rule).toMatch(/font-size:\s*120%\s*!important/i);
|
|
148
106
|
});
|
|
149
107
|
});
|
|
150
|
-
|
|
151
|
-
describe('click-to-select', () => {
|
|
152
|
-
it('selects this choice on click when nothing else is selected', () => {
|
|
153
|
-
const onSelectClick = jest.fn();
|
|
154
|
-
const { container } = render(<Choice {...defaultProps} selectedItem={null} onSelectClick={onSelectClick} />);
|
|
155
|
-
|
|
156
|
-
fireEvent.click(container.firstChild);
|
|
157
|
-
|
|
158
|
-
expect(onSelectClick).toHaveBeenCalledWith({
|
|
159
|
-
choice: defaultProps.choice,
|
|
160
|
-
instanceId: defaultProps.instanceId,
|
|
161
|
-
fromChoice: true,
|
|
162
|
-
type: 'MaskBlank',
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('toggles off (calls onSelectClick again) when clicking the already-selected choice', () => {
|
|
167
|
-
const onSelectClick = jest.fn();
|
|
168
|
-
const selectedItem = { choice: defaultProps.choice, instanceId: defaultProps.instanceId, fromChoice: true, type: 'MaskBlank' };
|
|
169
|
-
const { container } = render(
|
|
170
|
-
<Choice {...defaultProps} selectedItem={selectedItem} onSelectClick={onSelectClick} />,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
fireEvent.click(container.firstChild);
|
|
174
|
-
|
|
175
|
-
expect(onSelectClick).toHaveBeenCalledWith({
|
|
176
|
-
choice: defaultProps.choice,
|
|
177
|
-
instanceId: defaultProps.instanceId,
|
|
178
|
-
fromChoice: true,
|
|
179
|
-
type: 'MaskBlank',
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('selects this choice even while a different item is already selected (pool items are never placement targets)', () => {
|
|
184
|
-
const onSelectClick = jest.fn();
|
|
185
|
-
const selectedItem = { id: 'some-blank', fromChoice: false, type: 'MaskBlank' };
|
|
186
|
-
const { container } = render(
|
|
187
|
-
<Choice {...defaultProps} selectedItem={selectedItem} onSelectClick={onSelectClick} />,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
fireEvent.click(container.firstChild);
|
|
191
|
-
|
|
192
|
-
expect(onSelectClick).toHaveBeenCalledWith({
|
|
193
|
-
choice: defaultProps.choice,
|
|
194
|
-
instanceId: defaultProps.instanceId,
|
|
195
|
-
fromChoice: true,
|
|
196
|
-
type: 'MaskBlank',
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('does nothing on click when disabled', () => {
|
|
201
|
-
const onSelectClick = jest.fn();
|
|
202
|
-
const { container } = render(
|
|
203
|
-
<Choice {...defaultProps} disabled={true} selectedItem={null} onSelectClick={onSelectClick} />,
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
fireEvent.click(container.firstChild);
|
|
207
|
-
|
|
208
|
-
expect(onSelectClick).not.toHaveBeenCalled();
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
108
|
});
|
|
212
109
|
});
|
package/src/choices/choice.jsx
CHANGED
|
@@ -8,12 +8,12 @@ import { color } from '@pie-lib/render-ui';
|
|
|
8
8
|
|
|
9
9
|
export const DRAG_TYPE = 'MaskBlank';
|
|
10
10
|
|
|
11
|
-
const StyledChoice = styled('span')(({ theme, disabled
|
|
11
|
+
const StyledChoice = styled('span')(({ theme, disabled }) => ({
|
|
12
12
|
border: `solid 0px ${theme.palette.primary.main}`,
|
|
13
|
+
borderRadius: theme.spacing(2),
|
|
13
14
|
margin: theme.spacing(0.5),
|
|
14
15
|
transform: 'translate(0, 0)',
|
|
15
16
|
display: 'inline-flex',
|
|
16
|
-
opacity: selected ? 0.7 : 1,
|
|
17
17
|
...(disabled && {}),
|
|
18
18
|
}));
|
|
19
19
|
|
|
@@ -55,7 +55,7 @@ const StyledChipLabel = styled('span')(() => ({
|
|
|
55
55
|
},
|
|
56
56
|
}));
|
|
57
57
|
|
|
58
|
-
export default function Choice({ choice, disabled, instanceId
|
|
58
|
+
export default function Choice({ choice, disabled, instanceId }) {
|
|
59
59
|
const rootRef = useRef(null);
|
|
60
60
|
|
|
61
61
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
@@ -68,15 +68,6 @@ export default function Choice({ choice, disabled, instanceId, selectedItem, onS
|
|
|
68
68
|
renderMath(rootRef.current);
|
|
69
69
|
}, [choice.value]);
|
|
70
70
|
|
|
71
|
-
const isSelected = !!selectedItem && selectedItem.fromChoice === true && selectedItem.choice.id === choice.id;
|
|
72
|
-
|
|
73
|
-
const handleClick = (e) => {
|
|
74
|
-
if (disabled) return;
|
|
75
|
-
|
|
76
|
-
e.stopPropagation();
|
|
77
|
-
onSelectClick?.({ choice, instanceId, fromChoice: true, type: DRAG_TYPE });
|
|
78
|
-
};
|
|
79
|
-
|
|
80
71
|
return (
|
|
81
72
|
<StyledChoice
|
|
82
73
|
ref={setNodeRef}
|
|
@@ -89,8 +80,6 @@ export default function Choice({ choice, disabled, instanceId, selectedItem, onS
|
|
|
89
80
|
: {}
|
|
90
81
|
}
|
|
91
82
|
disabled={disabled}
|
|
92
|
-
selected={isSelected}
|
|
93
|
-
onClick={handleClick}
|
|
94
83
|
{...listeners}
|
|
95
84
|
{...attributes}
|
|
96
85
|
>
|
|
@@ -108,6 +97,4 @@ Choice.propTypes = {
|
|
|
108
97
|
choice: PropTypes.object.isRequired,
|
|
109
98
|
disabled: PropTypes.bool,
|
|
110
99
|
instanceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
111
|
-
selectedItem: PropTypes.object,
|
|
112
|
-
onSelectClick: PropTypes.func,
|
|
113
100
|
};
|
package/src/choices/index.jsx
CHANGED
|
@@ -12,9 +12,6 @@ export default class Choices extends React.Component {
|
|
|
12
12
|
value: PropTypes.object,
|
|
13
13
|
choicePosition: PropTypes.string.isRequired,
|
|
14
14
|
instanceId: PropTypes.string, // Added for drag isolation
|
|
15
|
-
selectedItem: PropTypes.object,
|
|
16
|
-
onSelectClick: PropTypes.func,
|
|
17
|
-
onPlacementClick: PropTypes.func,
|
|
18
15
|
};
|
|
19
16
|
|
|
20
17
|
getStyleForWrapper = () => {
|
|
@@ -43,22 +40,8 @@ export default class Choices extends React.Component {
|
|
|
43
40
|
}
|
|
44
41
|
};
|
|
45
42
|
|
|
46
|
-
handlePoolClick = () => {
|
|
47
|
-
const { disabled, selectedItem, onPlacementClick } = this.props;
|
|
48
|
-
|
|
49
|
-
if (disabled) return;
|
|
50
|
-
|
|
51
|
-
if (selectedItem) {
|
|
52
|
-
// `undefined` targetId means "the choice board" — drag-in-the-blank.jsx's
|
|
53
|
-
// commitPlacement routes it to removing the item from wherever it currently is.
|
|
54
|
-
onPlacementClick?.(undefined);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Pool background clicked with nothing selected: nothing to place.
|
|
58
|
-
};
|
|
59
|
-
|
|
60
43
|
render() {
|
|
61
|
-
const { disabled, duplicates, choices, value, instanceId
|
|
44
|
+
const { disabled, duplicates, choices, value, instanceId } = this.props;
|
|
62
45
|
const filteredChoices = choices.filter((c) => {
|
|
63
46
|
if (duplicates === true) {
|
|
64
47
|
return true;
|
|
@@ -69,17 +52,10 @@ export default class Choices extends React.Component {
|
|
|
69
52
|
const elementStyle = { ...this.getStyleForWrapper(), minWidth: '100px' };
|
|
70
53
|
|
|
71
54
|
return (
|
|
72
|
-
<div style={elementStyle}
|
|
55
|
+
<div style={elementStyle}>
|
|
73
56
|
<DragDroppablePlaceholder disabled={disabled} instanceId={instanceId}>
|
|
74
57
|
{filteredChoices.map((c, index) => (
|
|
75
|
-
<Choice
|
|
76
|
-
key={`${c.value}-${index}`}
|
|
77
|
-
disabled={disabled}
|
|
78
|
-
choice={c}
|
|
79
|
-
instanceId={instanceId}
|
|
80
|
-
selectedItem={selectedItem}
|
|
81
|
-
onSelectClick={onSelectClick}
|
|
82
|
-
/>
|
|
58
|
+
<Choice key={`${c.value}-${index}`} disabled={disabled} choice={c} instanceId={instanceId} />
|
|
83
59
|
))}
|
|
84
60
|
</DragDroppablePlaceholder>
|
|
85
61
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { render, screen, act
|
|
2
|
+
import { render, screen, act } from '@testing-library/react';
|
|
3
3
|
import Blank from '../blank';
|
|
4
4
|
|
|
5
5
|
// Mock @dnd-kit hooks to avoid DndContext requirement
|
|
6
6
|
jest.mock('@dnd-kit/core', () => ({
|
|
7
|
-
useDraggable: jest.fn((
|
|
8
|
-
attributes:
|
|
7
|
+
useDraggable: jest.fn(() => ({
|
|
8
|
+
attributes: {},
|
|
9
9
|
listeners: {},
|
|
10
10
|
setNodeRef: jest.fn(),
|
|
11
11
|
transform: null,
|
|
@@ -163,9 +163,8 @@ describe('Blank', () => {
|
|
|
163
163
|
jest.runAllTimers();
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
const wrapper = container.firstChild; // StyledContent
|
|
167
|
-
const
|
|
168
|
-
const chip = dragHandle && dragHandle.firstChild; // StyledChip (rootRef)
|
|
166
|
+
const wrapper = container.firstChild; // StyledContent
|
|
167
|
+
const chip = wrapper && wrapper.firstChild; // StyledChip (rootRef)
|
|
169
168
|
|
|
170
169
|
// Width and height should include padding (24px) around measured content
|
|
171
170
|
expect(chip.style.width).toBe('129px');
|
|
@@ -230,184 +229,4 @@ describe('Blank', () => {
|
|
|
230
229
|
// Should show visual feedback for drag over
|
|
231
230
|
});
|
|
232
231
|
});
|
|
233
|
-
|
|
234
|
-
describe('click-to-select and click-to-place', () => {
|
|
235
|
-
const { useDroppable } = require('@dnd-kit/core');
|
|
236
|
-
|
|
237
|
-
afterEach(() => {
|
|
238
|
-
useDroppable.mockReturnValue({ setNodeRef: jest.fn(), isOver: false, active: null });
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('is a native tab stop (role=button, tabIndex=0) when empty and not disabled', () => {
|
|
242
|
-
const { container } = render(<Blank {...defaultProps} choice={undefined} />);
|
|
243
|
-
const outer = container.firstChild;
|
|
244
|
-
|
|
245
|
-
expect(outer.getAttribute('role')).toBe('button');
|
|
246
|
-
expect(outer.getAttribute('tabindex')).toBe('0');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('is not a tab stop when it already holds a choice (relies on the inner draggable node)', () => {
|
|
250
|
-
const { container } = render(<Blank {...defaultProps} />);
|
|
251
|
-
const outer = container.firstChild;
|
|
252
|
-
|
|
253
|
-
expect(outer.getAttribute('role')).toBeNull();
|
|
254
|
-
expect(outer.getAttribute('tabindex')).toBe('-1');
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('is not a tab stop when disabled, even if empty', () => {
|
|
258
|
-
const { container } = render(<Blank {...defaultProps} choice={undefined} disabled={true} />);
|
|
259
|
-
const outer = container.firstChild;
|
|
260
|
-
|
|
261
|
-
expect(outer.getAttribute('tabindex')).toBe('-1');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('does not make the inner draggable node a second tab stop when the blank is empty', () => {
|
|
265
|
-
const { container } = render(<Blank {...defaultProps} choice={undefined} selectedItem={null} />);
|
|
266
|
-
const outer = container.firstChild;
|
|
267
|
-
const inner = outer.firstElementChild;
|
|
268
|
-
|
|
269
|
-
expect(outer.getAttribute('tabindex')).toBe('0');
|
|
270
|
-
// dragAttributes (which carries tabIndex) is only spread onto the inner node when
|
|
271
|
-
// it's the live tab stop (filled and not disabled) — otherwise it's omitted
|
|
272
|
-
// entirely, so no tabindex attribute is present at all here.
|
|
273
|
-
expect(inner.getAttribute('tabindex')).toBeNull();
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('makes the inner draggable node the tab stop when the blank is filled and not disabled', () => {
|
|
277
|
-
const { container } = render(<Blank {...defaultProps} selectedItem={null} />);
|
|
278
|
-
const outer = container.firstChild;
|
|
279
|
-
const inner = outer.firstElementChild;
|
|
280
|
-
|
|
281
|
-
expect(inner.getAttribute('tabindex')).toBe('0');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('selects this blank\'s content on click when nothing else is selected', () => {
|
|
285
|
-
const onSelectClick = jest.fn();
|
|
286
|
-
const { container } = render(
|
|
287
|
-
<Blank {...defaultProps} id="3" selectedItem={null} onSelectClick={onSelectClick} />,
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
fireEvent.click(container.firstChild);
|
|
291
|
-
|
|
292
|
-
expect(onSelectClick).toHaveBeenCalledWith({
|
|
293
|
-
id: '3',
|
|
294
|
-
choice: defaultProps.choice,
|
|
295
|
-
instanceId: undefined,
|
|
296
|
-
fromChoice: false,
|
|
297
|
-
type: 'MaskBlank',
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('toggles off when clicking its own already-selected content', () => {
|
|
302
|
-
const onSelectClick = jest.fn();
|
|
303
|
-
const selectedItem = { id: '3', choice: defaultProps.choice, instanceId: undefined, fromChoice: false, type: 'MaskBlank' };
|
|
304
|
-
const { container } = render(
|
|
305
|
-
<Blank {...defaultProps} id="3" selectedItem={selectedItem} onSelectClick={onSelectClick} />,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
fireEvent.click(container.firstChild);
|
|
309
|
-
|
|
310
|
-
expect(onSelectClick).toHaveBeenCalledWith(selectedItem);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('places the current selection here when clicking a different, already-filled blank', () => {
|
|
314
|
-
const onPlacementClick = jest.fn();
|
|
315
|
-
const selectedItem = { choice: { value: 'Other' }, instanceId: undefined, fromChoice: true, type: 'MaskBlank' };
|
|
316
|
-
const { container } = render(
|
|
317
|
-
<Blank {...defaultProps} id="3" selectedItem={selectedItem} onPlacementClick={onPlacementClick} />,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
fireEvent.click(container.firstChild);
|
|
321
|
-
|
|
322
|
-
expect(onPlacementClick).toHaveBeenCalledWith('3');
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('places the current selection here on Space/Enter when empty', () => {
|
|
326
|
-
const onPlacementClick = jest.fn();
|
|
327
|
-
const selectedItem = { choice: { value: 'Other' }, instanceId: undefined, fromChoice: true, type: 'MaskBlank' };
|
|
328
|
-
const { container } = render(
|
|
329
|
-
<Blank {...defaultProps} id="3" choice={undefined} selectedItem={selectedItem} onPlacementClick={onPlacementClick} />,
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
fireEvent.keyDown(container.firstChild, { code: 'Space' });
|
|
333
|
-
|
|
334
|
-
expect(onPlacementClick).toHaveBeenCalledWith('3');
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('does nothing on click when empty and nothing is selected', () => {
|
|
338
|
-
const onSelectClick = jest.fn();
|
|
339
|
-
const onPlacementClick = jest.fn();
|
|
340
|
-
const { container } = render(
|
|
341
|
-
<Blank {...defaultProps} choice={undefined} selectedItem={null} onSelectClick={onSelectClick} onPlacementClick={onPlacementClick} />,
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
fireEvent.click(container.firstChild);
|
|
345
|
-
|
|
346
|
-
expect(onSelectClick).not.toHaveBeenCalled();
|
|
347
|
-
expect(onPlacementClick).not.toHaveBeenCalled();
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('does nothing on click when disabled', () => {
|
|
351
|
-
const onSelectClick = jest.fn();
|
|
352
|
-
const selectedItem = { choice: { value: 'Other' }, instanceId: undefined, fromChoice: true, type: 'MaskBlank' };
|
|
353
|
-
const { container } = render(
|
|
354
|
-
<Blank {...defaultProps} disabled={true} selectedItem={selectedItem} onSelectClick={onSelectClick} />,
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
fireEvent.click(container.firstChild);
|
|
358
|
-
|
|
359
|
-
expect(onSelectClick).not.toHaveBeenCalled();
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('folds click-selection hover into the same highlight a live drag-over shows', () => {
|
|
363
|
-
const selectedItem = { choice: { value: 'Other' }, instanceId: undefined, fromChoice: true, type: 'MaskBlank' };
|
|
364
|
-
const { container } = render(<Blank {...defaultProps} id="3" selectedItem={selectedItem} />);
|
|
365
|
-
const outer = container.firstChild;
|
|
366
|
-
|
|
367
|
-
fireEvent.mouseEnter(outer);
|
|
368
|
-
// The "over" prop drives the same CSS the real isOver-driven highlight uses —
|
|
369
|
-
// assert via the rendered chip's className, since StyledContent forwards `over`.
|
|
370
|
-
// Re-render check: BlankContent receives the folded isOver as true while hovered
|
|
371
|
-
// with a selection active — verified indirectly via the "over" chip class it sets.
|
|
372
|
-
expect(screen.getByText('Cow').closest('.over')).not.toBeNull();
|
|
373
|
-
|
|
374
|
-
fireEvent.mouseLeave(outer);
|
|
375
|
-
expect(screen.getByText('Cow').closest('.over')).toBeNull();
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('shows a pointer cursor on hover when something is selected', () => {
|
|
379
|
-
const selectedItem = { choice: { value: 'Other' }, instanceId: undefined, fromChoice: true, type: 'MaskBlank' };
|
|
380
|
-
const { container } = render(<Blank {...defaultProps} id="3" selectedItem={selectedItem} />);
|
|
381
|
-
const outer = container.firstChild;
|
|
382
|
-
const className = Array.from(outer.classList).find((c) => c.startsWith('css-'));
|
|
383
|
-
|
|
384
|
-
const hoverRule = collectEmotionRules().find((r) => r.includes(`.${className}:hover`));
|
|
385
|
-
|
|
386
|
-
expect(hoverRule).toBeDefined();
|
|
387
|
-
expect(hoverRule).toMatch(/cursor:\s*pointer/);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('keeps the default cursor on hover when nothing is selected', () => {
|
|
391
|
-
const { container } = render(<Blank {...defaultProps} id="3" selectedItem={null} />);
|
|
392
|
-
const outer = container.firstChild;
|
|
393
|
-
const className = Array.from(outer.classList).find((c) => c.startsWith('css-'));
|
|
394
|
-
|
|
395
|
-
const hoverRule = collectEmotionRules().find((r) => r.includes(`.${className}:hover`));
|
|
396
|
-
|
|
397
|
-
expect(hoverRule).toBeUndefined();
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it('keeps the default cursor on hover when disabled, even with something selected', () => {
|
|
401
|
-
const selectedItem = { choice: { value: 'Other' }, instanceId: undefined, fromChoice: true, type: 'MaskBlank' };
|
|
402
|
-
const { container } = render(
|
|
403
|
-
<Blank {...defaultProps} id="3" disabled={true} selectedItem={selectedItem} />,
|
|
404
|
-
);
|
|
405
|
-
const outer = container.firstChild;
|
|
406
|
-
const className = Array.from(outer.classList).find((c) => c.startsWith('css-'));
|
|
407
|
-
|
|
408
|
-
const hoverRule = collectEmotionRules().find((r) => r.includes(`.${className}:hover`));
|
|
409
|
-
|
|
410
|
-
expect(hoverRule).toBeUndefined();
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
232
|
});
|
package/src/components/blank.jsx
CHANGED
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import { renderMath } from '@pie-lib/math-rendering';
|
|
4
4
|
import debug from 'debug';
|
|
5
5
|
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
|
6
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
6
7
|
import { styled } from '@mui/material/styles';
|
|
7
8
|
import Chip from '@mui/material/Chip';
|
|
8
9
|
import classnames from 'classnames';
|
|
@@ -11,32 +12,22 @@ import { grey } from '@mui/material/colors';
|
|
|
11
12
|
|
|
12
13
|
const log = debug('pie-lib:mask-markup:blank');
|
|
13
14
|
|
|
14
|
-
const StyledContent = styled('span')(({ dragged, over
|
|
15
|
+
const StyledContent = styled('span')(({ dragged, over }) => ({
|
|
15
16
|
border: `solid 0px ${color.primary()}`,
|
|
16
17
|
minWidth: '200px',
|
|
18
|
+
touchAction: 'none',
|
|
17
19
|
overflow: 'hidden',
|
|
18
20
|
whiteSpace: 'nowrap',
|
|
19
21
|
opacity: 1,
|
|
20
|
-
cursor: 'default',
|
|
21
22
|
...(over && {
|
|
22
23
|
whiteSpace: 'nowrap',
|
|
23
24
|
overflow: 'hidden',
|
|
24
25
|
}),
|
|
25
|
-
...(
|
|
26
|
+
...(dragged && {
|
|
26
27
|
opacity: 0.5,
|
|
27
28
|
}),
|
|
28
|
-
...(showsPointerCursor && {
|
|
29
|
-
'&:hover': {
|
|
30
|
-
cursor: 'pointer',
|
|
31
|
-
},
|
|
32
|
-
}),
|
|
33
29
|
}));
|
|
34
30
|
|
|
35
|
-
const StyledDragHandle = styled('span')({
|
|
36
|
-
display: 'inline-flex',
|
|
37
|
-
touchAction: 'none',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
31
|
const StyledChip = styled(Chip)(() => ({
|
|
41
32
|
backgroundColor: color.background(),
|
|
42
33
|
border: `2px dashed ${color.text()}`,
|
|
@@ -350,40 +341,23 @@ function DragDropBlank({
|
|
|
350
341
|
emptyResponseAreaWidth,
|
|
351
342
|
emptyResponseAreaHeight,
|
|
352
343
|
instanceId,
|
|
353
|
-
selectedItem,
|
|
354
|
-
onSelectClick,
|
|
355
|
-
onPlacementClick,
|
|
356
344
|
}) {
|
|
357
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
358
|
-
|
|
359
|
-
const dragData = {
|
|
360
|
-
id,
|
|
361
|
-
choice,
|
|
362
|
-
instanceId,
|
|
363
|
-
fromChoice: false, // This is from a blank, not from choices
|
|
364
|
-
type: 'MaskBlank',
|
|
365
|
-
};
|
|
366
|
-
|
|
367
345
|
// Setup draggable functionality
|
|
368
346
|
const {
|
|
369
347
|
attributes: dragAttributes,
|
|
370
348
|
listeners: dragListeners,
|
|
371
349
|
setNodeRef: setDragNodeRef,
|
|
350
|
+
transform,
|
|
372
351
|
isDragging,
|
|
373
352
|
} = useDraggable({
|
|
374
353
|
id: `mask-blank-drag-${id}`,
|
|
375
354
|
disabled: disabled || !choice,
|
|
376
|
-
data:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// no-op. Drop it out of the tab order here whenever it isn't independently
|
|
383
|
-
// reachable/interactive, leaving the outer wrapper (for an empty blank) or
|
|
384
|
-
// nothing (for a disabled blank) as the sole stop.
|
|
385
|
-
attributes: {
|
|
386
|
-
tabIndex: choice && !disabled ? 0 : -1,
|
|
355
|
+
data: {
|
|
356
|
+
id: id,
|
|
357
|
+
choice: choice,
|
|
358
|
+
instanceId: instanceId,
|
|
359
|
+
fromChoice: false, // This is from a blank, not from choices
|
|
360
|
+
type: 'MaskBlank',
|
|
387
361
|
},
|
|
388
362
|
});
|
|
389
363
|
|
|
@@ -401,82 +375,38 @@ function DragDropBlank({
|
|
|
401
375
|
},
|
|
402
376
|
});
|
|
403
377
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (isSelected) {
|
|
410
|
-
// Clicking this blank's already-selected content again deselects it.
|
|
411
|
-
onSelectClick?.(dragData);
|
|
412
|
-
} else if (selectedItem) {
|
|
413
|
-
// Something else is selected — place it here, whether this blank is currently
|
|
414
|
-
// empty or already filled (the same commitPlacement logic drag-and-drop uses).
|
|
415
|
-
onPlacementClick?.(id);
|
|
416
|
-
} else if (choice) {
|
|
417
|
-
// Nothing selected yet, and this blank holds an answer — select it for moving
|
|
418
|
-
// elsewhere, the same way Tab+Space/Enter does.
|
|
419
|
-
onSelectClick?.(dragData);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Empty blank clicked with nothing selected: nothing to place or select.
|
|
378
|
+
// Combine refs for both drag and drop
|
|
379
|
+
const setNodeRef = (node) => {
|
|
380
|
+
setDragNodeRef(node);
|
|
381
|
+
setDropNodeRef(node);
|
|
423
382
|
};
|
|
424
383
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// needs its own focus/activation handling so "select a choice, then Tab to a blank
|
|
428
|
-
// and press Space/Enter" works even when the blank is empty. This is independent of,
|
|
429
|
-
// and doesn't change, the existing in-drag Tab-cycling (that's driven by an active
|
|
430
|
-
// dnd-kit drag, not native focus).
|
|
431
|
-
//
|
|
432
|
-
// Only made a native Tab stop when NOT draggable (i.e. empty): when the blank is
|
|
433
|
-
// filled, the inner node is already independently tabbable via dnd-kit's own
|
|
434
|
-
// attributes for the existing pick-up-to-move gesture, and adding a second, outer Tab
|
|
435
|
-
// stop for the same visual chip would add an extra stop to the existing Tab order —
|
|
436
|
-
// the same double-tab-stop bug already found and fixed once in match-list/image-
|
|
437
|
-
// cloze-association's equivalent code.
|
|
438
|
-
const isNativeTabStop = !choice && !disabled;
|
|
439
|
-
const isInnerDraggable = !!choice && !disabled;
|
|
440
|
-
|
|
441
|
-
const handleKeyDown = (e) => {
|
|
442
|
-
if (e.code === 'Space' || e.code === 'Enter') {
|
|
443
|
-
e.preventDefault();
|
|
444
|
-
handleClick();
|
|
445
|
-
}
|
|
384
|
+
const style = {
|
|
385
|
+
transform: CSS.Translate.toString(transform),
|
|
446
386
|
};
|
|
447
387
|
|
|
448
|
-
const hasSelection = !!selectedItem;
|
|
449
|
-
const showsHoverEffect = isOver || (hasSelection && isHovered && !disabled);
|
|
450
|
-
|
|
451
388
|
return (
|
|
452
389
|
<StyledContent
|
|
453
|
-
ref={
|
|
454
|
-
|
|
455
|
-
tabIndex={isNativeTabStop ? 0 : -1}
|
|
456
|
-
onClick={handleClick}
|
|
457
|
-
onKeyDown={isNativeTabStop ? handleKeyDown : undefined}
|
|
458
|
-
onMouseEnter={() => setIsHovered(true)}
|
|
459
|
-
onMouseLeave={() => setIsHovered(false)}
|
|
390
|
+
ref={setNodeRef}
|
|
391
|
+
style={style}
|
|
460
392
|
dragged={isDragging}
|
|
461
|
-
over={
|
|
462
|
-
|
|
463
|
-
|
|
393
|
+
over={isOver}
|
|
394
|
+
{...dragAttributes}
|
|
395
|
+
{...dragListeners}
|
|
464
396
|
>
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
/>
|
|
479
|
-
</StyledDragHandle>
|
|
397
|
+
<BlankContent
|
|
398
|
+
id={id}
|
|
399
|
+
disabled={disabled}
|
|
400
|
+
duplicates={duplicates}
|
|
401
|
+
choice={choice}
|
|
402
|
+
isOver={isOver}
|
|
403
|
+
dragItem={dragItem?.data?.current}
|
|
404
|
+
correct={correct}
|
|
405
|
+
onChange={onChange}
|
|
406
|
+
emptyResponseAreaWidth={emptyResponseAreaWidth}
|
|
407
|
+
emptyResponseAreaHeight={emptyResponseAreaHeight}
|
|
408
|
+
instanceId={instanceId}
|
|
409
|
+
/>
|
|
480
410
|
</StyledContent>
|
|
481
411
|
);
|
|
482
412
|
}
|
|
@@ -496,9 +426,6 @@ DragDropBlank.propTypes = {
|
|
|
496
426
|
emptyResponseAreaWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
497
427
|
emptyResponseAreaHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
498
428
|
instanceId: PropTypes.string,
|
|
499
|
-
selectedItem: PropTypes.object,
|
|
500
|
-
onSelectClick: PropTypes.func,
|
|
501
|
-
onPlacementClick: PropTypes.func,
|
|
502
429
|
};
|
|
503
430
|
|
|
504
431
|
export default DragDropBlank;
|
|
@@ -13,8 +13,6 @@ import { styled } from '@mui/material/styles';
|
|
|
13
13
|
import { color } from '@pie-lib/render-ui';
|
|
14
14
|
import { renderMath } from '@pie-lib/math-rendering';
|
|
15
15
|
|
|
16
|
-
const pieVar = color.v('pie');
|
|
17
|
-
|
|
18
16
|
const StyledButton = styled(Button)(() => ({
|
|
19
17
|
color: color.text(),
|
|
20
18
|
border: `1px solid ${color.borderGray()}`,
|
|
@@ -55,11 +53,15 @@ const StyledButton = styled(Button)(() => ({
|
|
|
55
53
|
}));
|
|
56
54
|
|
|
57
55
|
const StyledMenu = styled(Menu)(() => ({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
backgroundColor: color.background(),
|
|
57
|
+
border: `1px solid ${color.correct()} !important`,
|
|
58
|
+
'&:hover': {
|
|
59
|
+
border: `1px solid ${color.text()} `,
|
|
60
|
+
borderColor: 'initial',
|
|
61
|
+
},
|
|
62
|
+
'&:focus': {
|
|
63
|
+
border: `1px solid ${color.text()}`,
|
|
64
|
+
borderColor: 'initial',
|
|
63
65
|
},
|
|
64
66
|
// remove default padding on the inner list
|
|
65
67
|
'& .MuiList-root': {
|