@pie-element/image-cloze-association 10.3.0 → 10.3.1-beta.2

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.
@@ -40,9 +40,6 @@ const BaseContainer = styled('div')(() => ({
40
40
  '&.baseIncorrect': {
41
41
  border: `2px solid ${color.incorrect()} !important`,
42
42
  },
43
- '&.selected': {
44
- opacity: 0.7,
45
- },
46
43
  }));
47
44
 
48
45
  const StyledSpan = styled(StaticHTMLSpan)(() => ({
@@ -53,17 +50,7 @@ const StyledSpan = styled(StaticHTMLSpan)(() => ({
53
50
  },
54
51
  }));
55
52
 
56
- const PossibleResponse = ({
57
- canDrag,
58
- containerStyle,
59
- data,
60
- onDragBegin,
61
- answerChoiceTransparency,
62
- isOverlay,
63
- selectedResponse,
64
- onSelectClick,
65
- onPlacementClick,
66
- }) => {
53
+ const PossibleResponse = ({ canDrag, containerStyle, data, onDragBegin, answerChoiceTransparency, isOverlay }) => {
67
54
  const rootRef = useRef(null);
68
55
  const longPressTimer = useRef(null);
69
56
 
@@ -77,31 +64,6 @@ const PossibleResponse = ({
77
64
  disabled: !canDrag,
78
65
  });
79
66
 
80
- const isSelected =
81
- !!selectedResponse && selectedResponse.id === data.id && selectedResponse.containerIndex === data.containerIndex;
82
-
83
- const handleClick = (e) => {
84
- if (!canDrag) return;
85
-
86
- e.stopPropagation();
87
-
88
- const isPlaced = data.containerIndex !== undefined;
89
-
90
- if (isSelected) {
91
- // Clicking the already-selected tile again deselects it.
92
- onSelectClick?.(data);
93
- } else if (selectedResponse && isPlaced) {
94
- // Something else is selected, and this tile is already inside a container:
95
- // place the selection into that same container (a swap/insert, handled by the
96
- // existing handleOnAnswerSelect logic).
97
- onPlacementClick?.(data.containerIndex);
98
- } else {
99
- // Nothing selected yet, or this is a pool item (pool items are never placement
100
- // targets — the pool itself, in possible-responses.jsx, is): select this tile.
101
- onSelectClick?.(data);
102
- }
103
- };
104
-
105
67
  const handleTouchEnd = () => {
106
68
  clearTimeout(longPressTimer.current);
107
69
  };
@@ -151,7 +113,6 @@ const PossibleResponse = ({
151
113
  answerChoiceTransparency: answerChoiceTransparency && !isDragging,
152
114
  [correctnessClass]: !!correctnessClass,
153
115
  textAnswerChoiceStyle: !containsImage && !isOverlay,
154
- selected: isSelected && !isDragging,
155
116
  });
156
117
 
157
118
  const promptClassNames = classNames({ hiddenSpan: data.hidden });
@@ -164,7 +125,6 @@ const PossibleResponse = ({
164
125
  rootRef.current = ref;
165
126
  setNodeRef(ref);
166
127
  }}
167
- onClick={handleClick}
168
128
  {...listeners}
169
129
  {...attributes}
170
130
  >
@@ -181,16 +141,12 @@ PossibleResponse.propTypes = {
181
141
  onDragBegin: PropTypes.func.isRequired,
182
142
  answerChoiceTransparency: PropTypes.bool,
183
143
  isOverlay: PropTypes.bool,
184
- selectedResponse: PropTypes.object,
185
- onSelectClick: PropTypes.func,
186
- onPlacementClick: PropTypes.func,
187
144
  };
188
145
 
189
146
  PossibleResponse.defaultProps = {
190
147
  containerStyle: {},
191
148
  answerChoiceTransparency: false,
192
149
  isOverlay: false,
193
- selectedResponse: null,
194
150
  };
195
151
 
196
152
  export default PossibleResponse;
@@ -22,40 +22,22 @@ const PossibleResponses = ({
22
22
  customStyle,
23
23
  isVertical,
24
24
  minHeight,
25
- selectedResponse,
26
- onSelectClick,
27
- onPlacementClick,
28
- }) => {
29
- const handlePoolClick = () => {
30
- if (!canDrag) return;
31
-
32
- if (selectedResponse) {
33
- // `undefined` containerIndex means "the pool" — root.jsx's placeSelectedResponse
34
- // routes it to handleOnAnswerRemove.
35
- onPlacementClick?.(undefined);
36
- }
37
- };
38
-
39
- return (
40
- <BaseContainer style={customStyle} onClick={handlePoolClick}>
41
- <ICADroppablePlaceholder id="ica-board" disabled={!canDrag} isVerticalPool={isVertical} minHeight={minHeight}>
42
- {(data || []).map((item) => (
43
- <PossibleResponse
44
- canDrag={canDrag}
45
- key={item.id}
46
- data={item}
47
- onDragBegin={onDragBegin}
48
- answerChoiceTransparency={answerChoiceTransparency}
49
- containerStyle={{ margin: '4px' }}
50
- selectedResponse={selectedResponse}
51
- onSelectClick={onSelectClick}
52
- onPlacementClick={onPlacementClick}
53
- />
54
- ))}
55
- </ICADroppablePlaceholder>
56
- </BaseContainer>
57
- );
58
- };
25
+ }) => (
26
+ <BaseContainer style={customStyle}>
27
+ <ICADroppablePlaceholder id="ica-board" disabled={!canDrag} isVerticalPool={isVertical} minHeight={minHeight}>
28
+ {(data || []).map((item) => (
29
+ <PossibleResponse
30
+ canDrag={canDrag}
31
+ key={item.id}
32
+ data={item}
33
+ onDragBegin={onDragBegin}
34
+ answerChoiceTransparency={answerChoiceTransparency}
35
+ containerStyle={{ margin: '4px' }}
36
+ />
37
+ ))}
38
+ </ICADroppablePlaceholder>
39
+ </BaseContainer>
40
+ );
59
41
 
60
42
  PossibleResponses.propTypes = {
61
43
  canDrag: PropTypes.bool.isRequired,
@@ -65,9 +47,6 @@ PossibleResponses.propTypes = {
65
47
  customStyle: PropTypes.object,
66
48
  isVertical: PropTypes.bool,
67
49
  minHeight: PropTypes.number,
68
- selectedResponse: PropTypes.object,
69
- onSelectClick: PropTypes.func,
70
- onPlacementClick: PropTypes.func,
71
50
  };
72
51
 
73
52
  export default PossibleResponses;
package/src/root.jsx CHANGED
@@ -16,16 +16,9 @@ import InteractiveSection from './interactive-section';
16
16
  import PossibleResponses from './possible-responses';
17
17
  import { getUnansweredAnswers, getAnswersCorrectness } from './utils-correctness';
18
18
  import PossibleResponse from './possible-response';
19
- import { closestDroppableKeyboardCoordinates } from './keyboard-coordinates';
20
19
 
21
20
  const generateId = () => Math.random().toString(36).substring(2) + new Date().getTime().toString(36);
22
21
 
23
- // A click that lands right after a real drag gesture ends (pointer drag-and-drop, or
24
- // the browser's own synthetic click for a keyboard Space/Enter) must be ignored by the
25
- // click-to-select/click-to-place handlers below, or it would immediately reopen or
26
- // re-trigger a selection for a drag that just completed.
27
- const CLICK_AFTER_DRAG_GUARD_MS = 250;
28
-
29
22
  const StyledUiLayout = styled(UiLayout)({
30
23
  color: color.text(),
31
24
  backgroundColor: color.background(),
@@ -83,9 +76,7 @@ export class ImageClozeAssociationComponent extends React.Component {
83
76
  maxResponsePerZone: maxResponsePerZone || 1,
84
77
  showCorrect: false,
85
78
  isValidDrop: false,
86
- selectedResponse: null,
87
79
  };
88
- this.lastDragEndAt = 0;
89
80
  }
90
81
 
91
82
  onDragStart = (event) => {
@@ -95,7 +86,6 @@ export class ImageClozeAssociationComponent extends React.Component {
95
86
  this.setState({
96
87
  draggingElement: active.data.current,
97
88
  isValidDrop: false,
98
- selectedResponse: active.data.current,
99
89
  });
100
90
  }
101
91
  };
@@ -122,9 +112,6 @@ export class ImageClozeAssociationComponent extends React.Component {
122
112
  isValidDrop: shouldDisableAnimation,
123
113
  });
124
114
 
125
- this.cancelSelection();
126
- this.lastDragEndAt = Date.now();
127
-
128
115
  if (!over || !active) {
129
116
  return;
130
117
  }
@@ -134,9 +121,7 @@ export class ImageClozeAssociationComponent extends React.Component {
134
121
  }
135
122
 
136
123
  if (over.id === 'ica-board') {
137
- if (draggedItem.containerIndex !== undefined) {
138
- this.handleOnAnswerRemove(draggedItem);
139
- }
124
+ this.handleOnAnswerRemove(draggedItem);
140
125
  return;
141
126
  }
142
127
 
@@ -145,94 +130,6 @@ export class ImageClozeAssociationComponent extends React.Component {
145
130
  }
146
131
  };
147
132
 
148
- onDragCancel = () => {
149
- this.setState({ draggingElement: { id: '', value: '' } });
150
- this.cancelSelection();
151
- this.lastDragEndAt = Date.now();
152
- };
153
-
154
- isSameResponse = (a, b) => !!a && !!b && a.id === b.id && a.containerIndex === b.containerIndex;
155
-
156
- // Click-to-select semantics: selecting the currently-selected response again clears
157
- // the selection instead of re-selecting it.
158
- toggleResponseSelection = (data) => {
159
- this.setState((state) => ({
160
- selectedResponse: this.isSameResponse(state.selectedResponse, data) ? null : data,
161
- }));
162
- };
163
-
164
- cancelSelection = () => {
165
- this.setState({ selectedResponse: null });
166
- };
167
-
168
- // If a real dnd-kit drag (started via keyboard Space/Enter) is still live when a
169
- // click completes the placement below, it needs to be cleanly ended — otherwise
170
- // dnd-kit would still think a drag is in progress. Escape is already configured as
171
- // this sensor's cancel key (see the keyboardCodes passed to DragProvider below), and
172
- // dispatching it as a real DOM KeyboardEvent is how dnd-kit's own document-level
173
- // listener is reached from outside its sensor.
174
- //
175
- // Only dispatch when a drag is actually live (draggingElement.id is truthy) — this is
176
- // a synthetic Escape keydown on `document`, so an unconditional dispatch would also be
177
- // observed by any other document-level Escape listener (host player modals/dialogs,
178
- // or another mounted instance of this same component) even when nothing here actually
179
- // needed cancelling.
180
- endAnyLiveKeyboardDrag = () => {
181
- if (!this.state.draggingElement.id) {
182
- return;
183
- }
184
-
185
- document.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true, cancelable: true }));
186
- };
187
-
188
- placeSelectedResponse = (containerIndex) => {
189
- const { selectedResponse } = this.state;
190
-
191
- if (!selectedResponse) {
192
- return;
193
- }
194
-
195
- if (containerIndex === undefined) {
196
- // Placing into the pool = removing it from wherever it currently is
197
- if (selectedResponse.containerIndex !== undefined) {
198
- this.handleOnAnswerRemove(selectedResponse);
199
- }
200
- } else {
201
- this.handleOnAnswerSelect(selectedResponse, containerIndex);
202
- }
203
-
204
- this.cancelSelection();
205
- this.endAnyLiveKeyboardDrag();
206
- this.lastDragEndAt = Date.now();
207
- };
208
-
209
- isClickSoonAfterDragEnd = () => Date.now() - this.lastDragEndAt < CLICK_AFTER_DRAG_GUARD_MS;
210
-
211
- onResponseClick = (data) => {
212
- if (this.isClickSoonAfterDragEnd()) {
213
- return;
214
- }
215
-
216
- // A click that selects/deselects/switches a tile must end any dnd-kit drag that's
217
- // still live from an earlier keyboard Space/Enter pick-up first — otherwise dnd-kit
218
- // keeps thinking that earlier item is being dragged (it ignores new sensor
219
- // activation while a drag is active) while selectedResponse visually points at
220
- // whatever this click just selected. Ending the stale drag first (rather than
221
- // after) matters: ending it also cancels the current selection as a side effect
222
- // (see onDragCancel above), so doing it before
223
- // toggleResponseSelection lets this click's own selection be the one that sticks.
224
- this.endAnyLiveKeyboardDrag();
225
- this.toggleResponseSelection(data);
226
- };
227
-
228
- onPlacementClick = (containerIndex) => {
229
- if (this.isClickSoonAfterDragEnd()) {
230
- return;
231
- }
232
-
233
- this.placeSelectedResponse(containerIndex);
234
- };
235
-
236
133
  renderDragOverlay = () => {
237
134
  const { draggingElement } = this.state;
238
135
  const { model } = this.props;
@@ -456,9 +353,6 @@ export class ImageClozeAssociationComponent extends React.Component {
456
353
  responseContainerPadding,
457
354
  imageDropTargetPadding,
458
355
  maxResponsePerZone,
459
- selectedResponse: this.state.selectedResponse,
460
- onSelectClick: this.onResponseClick,
461
- onPlacementClick: this.onPlacementClick,
462
356
  };
463
357
 
464
358
  const renderImage = () => (
@@ -487,22 +381,13 @@ export class ImageClozeAssociationComponent extends React.Component {
487
381
  }}
488
382
  isVertical={isVertical}
489
383
  minHeight={isVertical ? image?.height : undefined}
490
- selectedResponse={this.state.selectedResponse}
491
- onSelectClick={this.onResponseClick}
492
- onPlacementClick={this.onPlacementClick}
493
384
  />
494
385
  </React.Fragment>
495
386
  );
496
387
  };
497
388
 
498
389
  return (
499
- <DragProvider
500
- onDragStart={this.onDragStart}
501
- onDragEnd={this.onDragEnd}
502
- onDragCancel={this.onDragCancel}
503
- keyboardCoordinateGetter={closestDroppableKeyboardCoordinates}
504
- keyboardCodes={{ start: ['Space', 'Enter'], cancel: ['Escape'], end: ['Space', 'Enter'] }}
505
- >
390
+ <DragProvider onDragStart={this.onDragStart} onDragEnd={this.onDragEnd}>
506
391
  <StyledUiLayout extraCSSRules={extraCSSRules} id={'main-container'} fontSizeFactor={fontSizeFactor}>
507
392
  {showTeacherInstructions && (
508
393
  <StyledTeacherInstructions
@@ -1,111 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.closestDroppableKeyboardCoordinates = void 0;
7
- var _core = require("@dnd-kit/core");
8
- /**
9
- * Custom keyboard coordinate getter for image-cloze-association's Tab-based placement.
10
- *
11
- * Tab/Shift+Tab cycle the dragged answer directly onto the next/previous enabled
12
- * droppable — a response container (`response-container-{index}`) or the choices pool
13
- * (`ica-board`) — sorted by on-screen position (top-to-bottom, then left-to-right),
14
- * since response containers are absolutely positioned over an image rather than laid
15
- * out as a simple list.
16
- *
17
- * Arrow keys are delegated to dnd-kit's own `defaultKeyboardCoordinateGetter`, leaving
18
- * the existing free-form arrow-key dragging behavior completely unchanged.
19
- */
20
- const closestDroppableKeyboardCoordinates = (event, {
21
- context,
22
- currentCoordinates
23
- }) => {
24
- const {
25
- code
26
- } = event;
27
- const isTab = code === 'Tab';
28
- const isArrow = code === _core.KeyboardCode.Down || code === _core.KeyboardCode.Up || code === _core.KeyboardCode.Left || code === _core.KeyboardCode.Right;
29
- if (!isTab && !isArrow) {
30
- return undefined;
31
- }
32
- if (isArrow) {
33
- return (0, _core.defaultKeyboardCoordinateGetter)(event, {
34
- context,
35
- currentCoordinates
36
- });
37
- }
38
- event.preventDefault();
39
- const {
40
- droppableRects,
41
- droppableContainers,
42
- collisionRect
43
- } = context;
44
- if (!droppableRects || droppableRects.size === 0) {
45
- return currentCoordinates;
46
- }
47
- const targets = [];
48
- for (const [id, container] of droppableContainers) {
49
- if (container?.disabled) continue;
50
- const rect = droppableRects.get(id);
51
- if (!rect) continue;
52
- const center = {
53
- x: rect.left + rect.width / 2,
54
- y: rect.top + rect.height / 2
55
- };
56
- // Land the dragged item's own top-left corner at the target's center-left point,
57
- // rather than at the target's own top-left corner (avoids overshooting into a
58
- // neighboring droppable when the target is much wider than the dragged item).
59
- const dropPosition = {
60
- x: rect.left,
61
- y: rect.top + rect.height / 2
62
- };
63
- targets.push({
64
- id,
65
- rect,
66
- dropPosition,
67
- center
68
- });
69
- }
70
- if (targets.length === 0) {
71
- return currentCoordinates;
72
- }
73
- const reverse = event.shiftKey;
74
- targets.sort((a, b) => {
75
- if (Math.abs(a.center.y - b.center.y) > 10) return a.center.y - b.center.y;
76
- return a.center.x - b.center.x;
77
- });
78
-
79
- // Find the current target: whichever target's rect actually contains the dragged
80
- // item's own center. This holds regardless of how the dragged item's size compares
81
- // to the target's — a compact answer tile landed (per `dropPosition` above) at a much
82
- // wider container's left edge is still contained within that container's rect.
83
- // Comparing distances between reconstructed centers (as an earlier, buggy version of
84
- // this same logic in match-list did) breaks down for exactly that shape: a
85
- // reconstructed center can end up closer to a completely different droppable than to
86
- // the one the item is actually sitting on, so the *next* press re-matches the wrong
87
- // target and looks like it does nothing.
88
- const draggedCenter = collisionRect ? {
89
- x: collisionRect.left + collisionRect.width / 2,
90
- y: collisionRect.top + collisionRect.height / 2
91
- } : currentCoordinates;
92
- let currentIndex = targets.findIndex(t => draggedCenter.x >= t.rect.left && draggedCenter.x <= t.rect.right && draggedCenter.y >= t.rect.top && draggedCenter.y <= t.rect.bottom);
93
-
94
- // Fall back to nearest-by-dropPosition if the dragged item's center isn't strictly
95
- // inside any target (e.g. mid-flight after a free arrow-key move).
96
- if (currentIndex === -1) {
97
- let minDist = Infinity;
98
- for (let i = 0; i < targets.length; i++) {
99
- const dist = distance(currentCoordinates, targets[i].dropPosition);
100
- if (dist < minDist) {
101
- minDist = dist;
102
- currentIndex = i;
103
- }
104
- }
105
- }
106
- const nextIndex = reverse ? (currentIndex - 1 + targets.length) % targets.length : (currentIndex + 1) % targets.length;
107
- return targets[nextIndex].dropPosition;
108
- };
109
- exports.closestDroppableKeyboardCoordinates = closestDroppableKeyboardCoordinates;
110
- const distance = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
111
- //# sourceMappingURL=keyboard-coordinates.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"keyboard-coordinates.js","names":["_core","require","closestDroppableKeyboardCoordinates","event","context","currentCoordinates","code","isTab","isArrow","KeyboardCode","Down","Up","Left","Right","undefined","defaultKeyboardCoordinateGetter","preventDefault","droppableRects","droppableContainers","collisionRect","size","targets","id","container","disabled","rect","get","center","x","left","width","y","top","height","dropPosition","push","length","reverse","shiftKey","sort","a","b","Math","abs","draggedCenter","currentIndex","findIndex","t","right","bottom","minDist","Infinity","i","dist","distance","nextIndex","exports","sqrt"],"sources":["../src/keyboard-coordinates.js"],"sourcesContent":["import { defaultKeyboardCoordinateGetter, KeyboardCode } from '@dnd-kit/core';\n\n/**\n * Custom keyboard coordinate getter for image-cloze-association's Tab-based placement.\n *\n * Tab/Shift+Tab cycle the dragged answer directly onto the next/previous enabled\n * droppable — a response container (`response-container-{index}`) or the choices pool\n * (`ica-board`) — sorted by on-screen position (top-to-bottom, then left-to-right),\n * since response containers are absolutely positioned over an image rather than laid\n * out as a simple list.\n *\n * Arrow keys are delegated to dnd-kit's own `defaultKeyboardCoordinateGetter`, leaving\n * the existing free-form arrow-key dragging behavior completely unchanged.\n */\nexport const closestDroppableKeyboardCoordinates = (event, { context, currentCoordinates }) => {\n const { code } = event;\n const isTab = code === 'Tab';\n const isArrow =\n code === KeyboardCode.Down || code === KeyboardCode.Up || code === KeyboardCode.Left || code === KeyboardCode.Right;\n\n if (!isTab && !isArrow) {\n return undefined;\n }\n\n if (isArrow) {\n return defaultKeyboardCoordinateGetter(event, { context, currentCoordinates });\n }\n\n event.preventDefault();\n\n const { droppableRects, droppableContainers, collisionRect } = context;\n\n if (!droppableRects || droppableRects.size === 0) {\n return currentCoordinates;\n }\n\n const targets = [];\n\n for (const [id, container] of droppableContainers) {\n if (container?.disabled) continue;\n\n const rect = droppableRects.get(id);\n\n if (!rect) continue;\n\n const center = {\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height / 2,\n };\n // Land the dragged item's own top-left corner at the target's center-left point,\n // rather than at the target's own top-left corner (avoids overshooting into a\n // neighboring droppable when the target is much wider than the dragged item).\n const dropPosition = {\n x: rect.left,\n y: rect.top + rect.height / 2,\n };\n\n targets.push({ id, rect, dropPosition, center });\n }\n\n if (targets.length === 0) {\n return currentCoordinates;\n }\n\n const reverse = event.shiftKey;\n\n targets.sort((a, b) => {\n if (Math.abs(a.center.y - b.center.y) > 10) return a.center.y - b.center.y;\n return a.center.x - b.center.x;\n });\n\n // Find the current target: whichever target's rect actually contains the dragged\n // item's own center. This holds regardless of how the dragged item's size compares\n // to the target's — a compact answer tile landed (per `dropPosition` above) at a much\n // wider container's left edge is still contained within that container's rect.\n // Comparing distances between reconstructed centers (as an earlier, buggy version of\n // this same logic in match-list did) breaks down for exactly that shape: a\n // reconstructed center can end up closer to a completely different droppable than to\n // the one the item is actually sitting on, so the *next* press re-matches the wrong\n // target and looks like it does nothing.\n const draggedCenter = collisionRect\n ? { x: collisionRect.left + collisionRect.width / 2, y: collisionRect.top + collisionRect.height / 2 }\n : currentCoordinates;\n\n let currentIndex = targets.findIndex(\n (t) =>\n draggedCenter.x >= t.rect.left &&\n draggedCenter.x <= t.rect.right &&\n draggedCenter.y >= t.rect.top &&\n draggedCenter.y <= t.rect.bottom,\n );\n\n // Fall back to nearest-by-dropPosition if the dragged item's center isn't strictly\n // inside any target (e.g. mid-flight after a free arrow-key move).\n if (currentIndex === -1) {\n let minDist = Infinity;\n\n for (let i = 0; i < targets.length; i++) {\n const dist = distance(currentCoordinates, targets[i].dropPosition);\n\n if (dist < minDist) {\n minDist = dist;\n currentIndex = i;\n }\n }\n }\n\n const nextIndex = reverse\n ? (currentIndex - 1 + targets.length) % targets.length\n : (currentIndex + 1) % targets.length;\n\n return targets[nextIndex].dropPosition;\n};\n\nconst distance = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n"],"mappings":";;;;;;AAAA,IAAAA,KAAA,GAAAC,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMC,mCAAmC,GAAGA,CAACC,KAAK,EAAE;EAAEC,OAAO;EAAEC;AAAmB,CAAC,KAAK;EAC7F,MAAM;IAAEC;EAAK,CAAC,GAAGH,KAAK;EACtB,MAAMI,KAAK,GAAGD,IAAI,KAAK,KAAK;EAC5B,MAAME,OAAO,GACXF,IAAI,KAAKG,kBAAY,CAACC,IAAI,IAAIJ,IAAI,KAAKG,kBAAY,CAACE,EAAE,IAAIL,IAAI,KAAKG,kBAAY,CAACG,IAAI,IAAIN,IAAI,KAAKG,kBAAY,CAACI,KAAK;EAErH,IAAI,CAACN,KAAK,IAAI,CAACC,OAAO,EAAE;IACtB,OAAOM,SAAS;EAClB;EAEA,IAAIN,OAAO,EAAE;IACX,OAAO,IAAAO,qCAA+B,EAACZ,KAAK,EAAE;MAAEC,OAAO;MAAEC;IAAmB,CAAC,CAAC;EAChF;EAEAF,KAAK,CAACa,cAAc,CAAC,CAAC;EAEtB,MAAM;IAAEC,cAAc;IAAEC,mBAAmB;IAAEC;EAAc,CAAC,GAAGf,OAAO;EAEtE,IAAI,CAACa,cAAc,IAAIA,cAAc,CAACG,IAAI,KAAK,CAAC,EAAE;IAChD,OAAOf,kBAAkB;EAC3B;EAEA,MAAMgB,OAAO,GAAG,EAAE;EAElB,KAAK,MAAM,CAACC,EAAE,EAAEC,SAAS,CAAC,IAAIL,mBAAmB,EAAE;IACjD,IAAIK,SAAS,EAAEC,QAAQ,EAAE;IAEzB,MAAMC,IAAI,GAAGR,cAAc,CAACS,GAAG,CAACJ,EAAE,CAAC;IAEnC,IAAI,CAACG,IAAI,EAAE;IAEX,MAAME,MAAM,GAAG;MACbC,CAAC,EAAEH,IAAI,CAACI,IAAI,GAAGJ,IAAI,CAACK,KAAK,GAAG,CAAC;MAC7BC,CAAC,EAAEN,IAAI,CAACO,GAAG,GAAGP,IAAI,CAACQ,MAAM,GAAG;IAC9B,CAAC;IACD;IACA;IACA;IACA,MAAMC,YAAY,GAAG;MACnBN,CAAC,EAAEH,IAAI,CAACI,IAAI;MACZE,CAAC,EAAEN,IAAI,CAACO,GAAG,GAAGP,IAAI,CAACQ,MAAM,GAAG;IAC9B,CAAC;IAEDZ,OAAO,CAACc,IAAI,CAAC;MAAEb,EAAE;MAAEG,IAAI;MAAES,YAAY;MAAEP;IAAO,CAAC,CAAC;EAClD;EAEA,IAAIN,OAAO,CAACe,MAAM,KAAK,CAAC,EAAE;IACxB,OAAO/B,kBAAkB;EAC3B;EAEA,MAAMgC,OAAO,GAAGlC,KAAK,CAACmC,QAAQ;EAE9BjB,OAAO,CAACkB,IAAI,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAK;IACrB,IAAIC,IAAI,CAACC,GAAG,CAACH,CAAC,CAACb,MAAM,CAACI,CAAC,GAAGU,CAAC,CAACd,MAAM,CAACI,CAAC,CAAC,GAAG,EAAE,EAAE,OAAOS,CAAC,CAACb,MAAM,CAACI,CAAC,GAAGU,CAAC,CAACd,MAAM,CAACI,CAAC;IAC1E,OAAOS,CAAC,CAACb,MAAM,CAACC,CAAC,GAAGa,CAAC,CAACd,MAAM,CAACC,CAAC;EAChC,CAAC,CAAC;;EAEF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAMgB,aAAa,GAAGzB,aAAa,GAC/B;IAAES,CAAC,EAAET,aAAa,CAACU,IAAI,GAAGV,aAAa,CAACW,KAAK,GAAG,CAAC;IAAEC,CAAC,EAAEZ,aAAa,CAACa,GAAG,GAAGb,aAAa,CAACc,MAAM,GAAG;EAAE,CAAC,GACpG5B,kBAAkB;EAEtB,IAAIwC,YAAY,GAAGxB,OAAO,CAACyB,SAAS,CACjCC,CAAC,IACAH,aAAa,CAAChB,CAAC,IAAImB,CAAC,CAACtB,IAAI,CAACI,IAAI,IAC9Be,aAAa,CAAChB,CAAC,IAAImB,CAAC,CAACtB,IAAI,CAACuB,KAAK,IAC/BJ,aAAa,CAACb,CAAC,IAAIgB,CAAC,CAACtB,IAAI,CAACO,GAAG,IAC7BY,aAAa,CAACb,CAAC,IAAIgB,CAAC,CAACtB,IAAI,CAACwB,MAC9B,CAAC;;EAED;EACA;EACA,IAAIJ,YAAY,KAAK,CAAC,CAAC,EAAE;IACvB,IAAIK,OAAO,GAAGC,QAAQ;IAEtB,KAAK,IAAIC,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAG/B,OAAO,CAACe,MAAM,EAAEgB,CAAC,EAAE,EAAE;MACvC,MAAMC,IAAI,GAAGC,QAAQ,CAACjD,kBAAkB,EAAEgB,OAAO,CAAC+B,CAAC,CAAC,CAAClB,YAAY,CAAC;MAElE,IAAImB,IAAI,GAAGH,OAAO,EAAE;QAClBA,OAAO,GAAGG,IAAI;QACdR,YAAY,GAAGO,CAAC;MAClB;IACF;EACF;EAEA,MAAMG,SAAS,GAAGlB,OAAO,GACrB,CAACQ,YAAY,GAAG,CAAC,GAAGxB,OAAO,CAACe,MAAM,IAAIf,OAAO,CAACe,MAAM,GACpD,CAACS,YAAY,GAAG,CAAC,IAAIxB,OAAO,CAACe,MAAM;EAEvC,OAAOf,OAAO,CAACkC,SAAS,CAAC,CAACrB,YAAY;AACxC,CAAC;AAACsB,OAAA,CAAAtD,mCAAA,GAAAA,mCAAA;AAEF,MAAMoD,QAAQ,GAAGA,CAACd,CAAC,EAAEC,CAAC,KAAKC,IAAI,CAACe,IAAI,CAAC,CAACjB,CAAC,CAACZ,CAAC,GAAGa,CAAC,CAACb,CAAC,KAAK,CAAC,GAAG,CAACY,CAAC,CAACT,CAAC,GAAGU,CAAC,CAACV,CAAC,KAAK,CAAC,CAAC","ignoreList":[]}
@@ -1,169 +0,0 @@
1
- import React from 'react';
2
- import { render, fireEvent } from '@testing-library/react';
3
- import ImageDropTarget from '../image-drop-target';
4
-
5
- jest.mock('@dnd-kit/core', () => ({
6
- useDroppable: () => ({
7
- setNodeRef: jest.fn(),
8
- isOver: false,
9
- }),
10
- }));
11
-
12
- jest.mock('../possible-response', () => (props) => (
13
- <div data-testid={`possible-response-${props.data.id}`} onClick={() => props.onSelectClick?.(props.data)}>
14
- {props.data.value}
15
- </div>
16
- ));
17
-
18
- describe('ImageDropTarget', () => {
19
- const baseProps = {
20
- answers: [],
21
- canDrag: true,
22
- containerStyle: {},
23
- draggingElement: { id: '' },
24
- onDragAnswerBegin: jest.fn(),
25
- onDragAnswerEnd: jest.fn(),
26
- onDrop: jest.fn(),
27
- index: 2,
28
- };
29
-
30
- describe('empty container', () => {
31
- it('is a native tab stop (role=button, tabIndex=0) when canDrag', () => {
32
- const { container } = render(<ImageDropTarget {...baseProps} />);
33
- const target = container.firstChild;
34
-
35
- expect(target.getAttribute('role')).toBe('button');
36
- expect(target.getAttribute('tabindex')).toBe('0');
37
- });
38
-
39
- it('is not a tab stop when canDrag is false, and has no role either (I2)', () => {
40
- const { container } = render(<ImageDropTarget {...baseProps} canDrag={false} />);
41
- const target = container.firstChild;
42
-
43
- expect(target.getAttribute('tabindex')).toBe('-1');
44
- expect(target.getAttribute('role')).toBeNull();
45
- });
46
-
47
- it('places the selection here on click', () => {
48
- const onPlacementClick = jest.fn();
49
- const { container } = render(
50
- <ImageDropTarget
51
- {...baseProps}
52
- selectedResponse={{ id: '9', containerIndex: undefined }}
53
- onPlacementClick={onPlacementClick}
54
- />,
55
- );
56
-
57
- fireEvent.click(container.firstChild);
58
-
59
- expect(onPlacementClick).toHaveBeenCalledWith(2);
60
- });
61
-
62
- it('does nothing on click when nothing is selected', () => {
63
- const onPlacementClick = jest.fn();
64
- const { container } = render(<ImageDropTarget {...baseProps} onPlacementClick={onPlacementClick} />);
65
-
66
- fireEvent.click(container.firstChild);
67
-
68
- expect(onPlacementClick).not.toHaveBeenCalled();
69
- });
70
-
71
- it('places the selection here on Space/Enter', () => {
72
- const onPlacementClick = jest.fn();
73
- const { container } = render(
74
- <ImageDropTarget
75
- {...baseProps}
76
- selectedResponse={{ id: '9', containerIndex: undefined }}
77
- onPlacementClick={onPlacementClick}
78
- />,
79
- );
80
-
81
- fireEvent.keyDown(container.firstChild, { code: 'Space' });
82
-
83
- expect(onPlacementClick).toHaveBeenCalledWith(2);
84
- });
85
- });
86
-
87
- describe('occupied container', () => {
88
- const answers = [{ id: '7', value: 'Placed', containerIndex: 2 }];
89
-
90
- it('is not its own tab stop and has no role either — occupied containers must not become unlabeled buttons for screen readers (I2)', () => {
91
- const { container } = render(<ImageDropTarget {...baseProps} answers={answers} />);
92
- const target = container.firstChild;
93
-
94
- expect(target.getAttribute('tabindex')).toBe('-1');
95
- expect(target.getAttribute('role')).toBeNull();
96
- });
97
-
98
- it('places the selection here when clicking empty background (not a specific tile)', () => {
99
- const onPlacementClick = jest.fn();
100
- const { container } = render(
101
- <ImageDropTarget
102
- {...baseProps}
103
- answers={answers}
104
- selectedResponse={{ id: '9', containerIndex: undefined }}
105
- onPlacementClick={onPlacementClick}
106
- />,
107
- );
108
-
109
- fireEvent.click(container.firstChild);
110
-
111
- expect(onPlacementClick).toHaveBeenCalledWith(2);
112
- });
113
-
114
- it('forwards selection props to the placed tile, which handles its own click', () => {
115
- const onSelectClick = jest.fn();
116
- const { getByTestId } = render(
117
- <ImageDropTarget {...baseProps} answers={answers} onSelectClick={onSelectClick} />,
118
- );
119
-
120
- fireEvent.click(getByTestId('possible-response-7'));
121
-
122
- expect(onSelectClick).toHaveBeenCalledWith(answers[0]);
123
- });
124
- });
125
-
126
- describe('hover affordance during click-to-select (I3)', () => {
127
- it('gets the is-over highlight and a pointer cursor on hover when something is selected', () => {
128
- const { container } = render(
129
- <ImageDropTarget {...baseProps} selectedResponse={{ id: '9', containerIndex: undefined }} />,
130
- );
131
- const target = container.firstChild;
132
-
133
- expect(target.className).not.toMatch(/\bis-over\b/);
134
-
135
- fireEvent.mouseEnter(target);
136
- expect(target.className).toMatch(/\bis-over\b/);
137
- expect(target.style.cursor).toBe('pointer');
138
-
139
- fireEvent.mouseLeave(target);
140
- expect(target.className).not.toMatch(/\bis-over\b/);
141
- });
142
-
143
- it('does not show the is-over highlight on hover when nothing is selected', () => {
144
- const { container } = render(<ImageDropTarget {...baseProps} selectedResponse={null} />);
145
- const target = container.firstChild;
146
-
147
- fireEvent.mouseEnter(target);
148
-
149
- expect(target.className).not.toMatch(/\bis-over\b/);
150
- expect(target.style.cursor).toBe('');
151
- });
152
-
153
- it('does not show the is-over highlight on hover when canDrag is false, even with a selection', () => {
154
- const { container } = render(
155
- <ImageDropTarget
156
- {...baseProps}
157
- canDrag={false}
158
- selectedResponse={{ id: '9', containerIndex: undefined }}
159
- />,
160
- );
161
- const target = container.firstChild;
162
-
163
- fireEvent.mouseEnter(target);
164
-
165
- expect(target.className).not.toMatch(/\bis-over\b/);
166
- expect(target.style.cursor).toBe('');
167
- });
168
- });
169
- });