@celestia-island/hikari 0.43.8 → 0.43.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/package.json
CHANGED
|
@@ -1547,7 +1547,7 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1547
1547
|
expect(tagTexts(container)).toEqual(["Technology", "Germany", "News"]);
|
|
1548
1548
|
});
|
|
1549
1549
|
|
|
1550
|
-
it("
|
|
1550
|
+
it("follows the row it grabbed when the host reorders the list under it", async () => {
|
|
1551
1551
|
const { events, container, model } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1552
1552
|
await openPanel(container);
|
|
1553
1553
|
const selected = reorderableRows();
|
|
@@ -1555,8 +1555,9 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1555
1555
|
|
|
1556
1556
|
holdPointer({ x: 10, y: 10 }, { x: 10, y: 130 }, grip(selected[0]));
|
|
1557
1557
|
await settle();
|
|
1558
|
-
// The host rewrites its own order out of band while the drag is held:
|
|
1559
|
-
//
|
|
1558
|
+
// The host rewrites its own order out of band while the drag is held: the
|
|
1559
|
+
// index the drag landed at names a different row now, but the row the
|
|
1560
|
+
// pointer is holding is still the one being moved.
|
|
1560
1561
|
model.value = ["de", "news", "tech"];
|
|
1561
1562
|
await settle();
|
|
1562
1563
|
expect(rowLabels().slice(0, 3)).toEqual(["Germany", "News", "Technology"]);
|
|
@@ -1564,10 +1565,35 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1564
1565
|
releasePointer({ x: 10, y: 130 });
|
|
1565
1566
|
await settle();
|
|
1566
1567
|
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
|
|
1570
|
-
expect(
|
|
1568
|
+
// "News" — the row the pointer grabbed — moved to the slot it was dragged
|
|
1569
|
+
// to, in the strip's own new order: the released gesture is never
|
|
1570
|
+
// silently replaced by a move of whichever row slid into that index.
|
|
1571
|
+
expect(events.modelValue.at(-1)).toEqual(["de", "tech", "news"]);
|
|
1572
|
+
expect(tagTexts(container)).toEqual(["Germany", "Technology", "News"]);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
it("keeps a dragged chip on the chip it grabbed when a × removes another one mid-drag", async () => {
|
|
1576
|
+
const { events, container } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1577
|
+
const strip = tags(container);
|
|
1578
|
+
pinStrip(strip, 60, "x");
|
|
1579
|
+
|
|
1580
|
+
// A second pair of eyes on the same list: the × of the FIRST chip is
|
|
1581
|
+
// clicked while the SECOND is being dragged. The field re-lays out under
|
|
1582
|
+
// the held pointer and the dragged chip slides one index left — so the
|
|
1583
|
+
// index the press landed at names the wrong chip by the time the pointer
|
|
1584
|
+
// comes up, and the drop has to stay on the chip the pointer is holding.
|
|
1585
|
+
holdPointer({ x: 70, y: 10 }, { x: 200, y: 10 }, strip[1]);
|
|
1586
|
+
await settle();
|
|
1587
|
+
closeButtons(container)[0].click();
|
|
1588
|
+
await settle();
|
|
1589
|
+
expect(tagTexts(container), "the × landed").toEqual(["Technology", "Germany"]);
|
|
1590
|
+
|
|
1591
|
+
releasePointer({ x: 200, y: 10 });
|
|
1592
|
+
await settle();
|
|
1593
|
+
|
|
1594
|
+
// "Technology" — the grabbed chip — lands last, in the field's own order.
|
|
1595
|
+
expect(events.modelValue.at(-1)).toEqual(["de", "tech"]);
|
|
1596
|
+
expect(tagTexts(container)).toEqual(["Germany", "Technology"]);
|
|
1571
1597
|
});
|
|
1572
1598
|
|
|
1573
1599
|
it("clamps a row drag to the selected group", async () => {
|
|
@@ -460,14 +460,15 @@ export const HkTagInput = defineComponent({
|
|
|
460
460
|
const stop = activeStop.value;
|
|
461
461
|
if (props.disabled || !stop || stop.kind !== "option") return false;
|
|
462
462
|
// A live press OWNS the order — either list's, and from the moment
|
|
463
|
-
// the press lands rather than from the moment it turns into a drag
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// the
|
|
463
|
+
// the press lands rather than from the moment it turns into a drag.
|
|
464
|
+
// The drag would survive a reorder (it moves the ELEMENT the press
|
|
465
|
+
// grabbed, re-reading its index as it goes), but the item's geometry
|
|
466
|
+
// is the one the press read: reordering the strip under a held pointer
|
|
467
|
+
// leaves the drag resolving against a place its item no longer
|
|
468
|
+
// occupies, which is how a gesture ends up quietly doing nothing. So
|
|
469
|
+
// the chord is inert while a pointer owns the list. It is still
|
|
470
|
+
// consumed by the caller either way (Alt+Arrow is not a plain arrow,
|
|
471
|
+
// and the browser must not act on it).
|
|
471
472
|
if (chipDrag.pressed.value || rowDrag.pressed.value) return false;
|
|
472
473
|
const group = selectedRows.value;
|
|
473
474
|
const from = group.findIndex((option) => option.key === stop.option.key);
|
|
@@ -218,6 +218,172 @@ function scrollStrip(
|
|
|
218
218
|
return { container, items };
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/** A strip whose frame IS the scroller it sits in — the frame's box, its
|
|
222
|
+
* LAID-OUT size (`offsetWidth`/`offsetHeight`, which no transform touches)
|
|
223
|
+
* and its scroll offsets all answer as a browser would, and `scene` lets a
|
|
224
|
+
* test scale and scroll the whole thing mid-gesture. */
|
|
225
|
+
function scrollerStrip(
|
|
226
|
+
count: number,
|
|
227
|
+
size: number,
|
|
228
|
+
height: number,
|
|
229
|
+
): {
|
|
230
|
+
frame: HTMLElement;
|
|
231
|
+
items: HTMLElement[];
|
|
232
|
+
scene: { scale: number; scroll: number; laid: number };
|
|
233
|
+
} {
|
|
234
|
+
const frame = document.createElement("div");
|
|
235
|
+
document.body.appendChild(frame);
|
|
236
|
+
const scene = { scale: 1, scroll: 0, laid: count * size };
|
|
237
|
+
Object.defineProperty(frame, "offsetWidth", { configurable: true, get: () => scene.laid });
|
|
238
|
+
Object.defineProperty(frame, "offsetHeight", { configurable: true, get: () => height });
|
|
239
|
+
Object.defineProperty(frame, "scrollLeft", { configurable: true, get: () => scene.scroll });
|
|
240
|
+
Object.defineProperty(frame, "scrollTop", { configurable: true, get: () => 0 });
|
|
241
|
+
Object.defineProperty(frame, "getBoundingClientRect", {
|
|
242
|
+
configurable: true,
|
|
243
|
+
value: () =>
|
|
244
|
+
asRect({
|
|
245
|
+
left: 0,
|
|
246
|
+
right: scene.laid * scene.scale,
|
|
247
|
+
top: 0,
|
|
248
|
+
bottom: height * scene.scale,
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
const items = Array.from({ length: count }, (_, i) => {
|
|
252
|
+
const el = document.createElement("div");
|
|
253
|
+
Object.defineProperty(el, "getBoundingClientRect", {
|
|
254
|
+
configurable: true,
|
|
255
|
+
value: () => {
|
|
256
|
+
const start = (i * size - scene.scroll) * scene.scale;
|
|
257
|
+
return asRect({
|
|
258
|
+
left: start,
|
|
259
|
+
right: start + size * scene.scale,
|
|
260
|
+
top: 0,
|
|
261
|
+
bottom: height * scene.scale,
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
frame.appendChild(el);
|
|
266
|
+
return el;
|
|
267
|
+
});
|
|
268
|
+
return { frame, items, scene };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A strip a BROWSER could have laid out: every element reports both of the
|
|
272
|
+
* readings a real engine keeps apart — the box it is DRAWN in
|
|
273
|
+
* (`getBoundingClientRect`, which every transform moves) and the box it is
|
|
274
|
+
* LAID OUT in (`offsetLeft`/`offsetTop`/`offsetWidth`/`offsetHeight` along
|
|
275
|
+
* an `offsetParent` chain, which no transform touches). One model drives
|
|
276
|
+
* both, exactly as a browser would, so a test can re-lay the strip out (a
|
|
277
|
+
* reorder, an insertion, a font change) or paint a transform on it and see
|
|
278
|
+
* which reading the resolution followed. */
|
|
279
|
+
function laidStrip(
|
|
280
|
+
layout: Box[],
|
|
281
|
+
frameLayout: { left: number; top: number; w: number; h: number },
|
|
282
|
+
frameDrawn: Box,
|
|
283
|
+
scene: {
|
|
284
|
+
scale?: number;
|
|
285
|
+
scroll?: number;
|
|
286
|
+
border?: { left: number; top: number };
|
|
287
|
+
/** The frame is `position: static`, so it is nobody's `offsetParent`:
|
|
288
|
+
* the items' layout numbers are then measured from further up — the
|
|
289
|
+
* fact that decides whether the frame's border still has to be added. */
|
|
290
|
+
staticFrame?: boolean;
|
|
291
|
+
} = {},
|
|
292
|
+
): {
|
|
293
|
+
frame: HTMLElement;
|
|
294
|
+
items: HTMLElement[];
|
|
295
|
+
laid: Box[];
|
|
296
|
+
relayout: (next: Box[]) => void;
|
|
297
|
+
setFrameDrawn: (next: Box) => void;
|
|
298
|
+
} {
|
|
299
|
+
const frame = document.createElement("div");
|
|
300
|
+
document.body.appendChild(frame);
|
|
301
|
+
const live = layout.map((box) => ({ ...box }));
|
|
302
|
+
const pins: Array<() => void> = [];
|
|
303
|
+
const border = scene.border ?? { left: 0, top: 0 };
|
|
304
|
+
const sceneOf = () => ({ scale: scene.scale ?? 1, scroll: scene.scroll ?? 0 });
|
|
305
|
+
frame.style.borderLeftWidth = `${border.left}px`;
|
|
306
|
+
frame.style.borderTopWidth = `${border.top}px`;
|
|
307
|
+
|
|
308
|
+
/** The box a browser draws for a laid-out box inside this frame. */
|
|
309
|
+
const drawn = (box: Box): Box => {
|
|
310
|
+
const { scale, scroll } = sceneOf();
|
|
311
|
+
const left = frameDrawn.left + (box.left - frameLayout.left - scroll) * scale;
|
|
312
|
+
const top = frameDrawn.top + (box.top - frameLayout.top) * scale;
|
|
313
|
+
return {
|
|
314
|
+
left,
|
|
315
|
+
right: left + (box.right - box.left) * scale,
|
|
316
|
+
top,
|
|
317
|
+
bottom: top + (box.bottom - box.top) * scale,
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
let frameDrawnBox = frameDrawn;
|
|
322
|
+
Object.defineProperty(frame, "getBoundingClientRect", {
|
|
323
|
+
configurable: true,
|
|
324
|
+
value: () => asRect(frameDrawnBox),
|
|
325
|
+
});
|
|
326
|
+
Object.defineProperty(frame, "offsetWidth", { configurable: true, get: () => frameLayout.w });
|
|
327
|
+
Object.defineProperty(frame, "offsetHeight", { configurable: true, get: () => frameLayout.h });
|
|
328
|
+
Object.defineProperty(frame, "offsetParent", { configurable: true, get: () => null });
|
|
329
|
+
Object.defineProperty(frame, "offsetLeft", { configurable: true, get: () => frameLayout.left });
|
|
330
|
+
Object.defineProperty(frame, "offsetTop", { configurable: true, get: () => frameLayout.top });
|
|
331
|
+
Object.defineProperty(frame, "scrollLeft", { configurable: true, get: () => sceneOf().scroll });
|
|
332
|
+
Object.defineProperty(frame, "scrollTop", { configurable: true, get: () => 0 });
|
|
333
|
+
|
|
334
|
+
const items = live.map((box, i) => {
|
|
335
|
+
const el = document.createElement("div");
|
|
336
|
+
frame.appendChild(el);
|
|
337
|
+
Object.defineProperty(el, "offsetParent", {
|
|
338
|
+
configurable: true,
|
|
339
|
+
get: () => (scene.staticFrame ? null : frame),
|
|
340
|
+
});
|
|
341
|
+
// A positioned frame is the items' offsetParent, so their offsets are
|
|
342
|
+
// measured from its PADDING edge; a static one is skipped and they are
|
|
343
|
+
// measured from the same place as the frame itself.
|
|
344
|
+
Object.defineProperty(el, "offsetLeft", {
|
|
345
|
+
configurable: true,
|
|
346
|
+
get: () =>
|
|
347
|
+
scene.staticFrame ? live[i].left : live[i].left - frameLayout.left - border.left,
|
|
348
|
+
});
|
|
349
|
+
Object.defineProperty(el, "offsetTop", {
|
|
350
|
+
configurable: true,
|
|
351
|
+
get: () => (scene.staticFrame ? live[i].top : live[i].top - frameLayout.top - border.top),
|
|
352
|
+
});
|
|
353
|
+
Object.defineProperty(el, "offsetWidth", {
|
|
354
|
+
configurable: true,
|
|
355
|
+
get: () => live[i].right - live[i].left,
|
|
356
|
+
});
|
|
357
|
+
Object.defineProperty(el, "offsetHeight", {
|
|
358
|
+
configurable: true,
|
|
359
|
+
get: () => live[i].bottom - live[i].top,
|
|
360
|
+
});
|
|
361
|
+
const pin = () =>
|
|
362
|
+
Object.defineProperty(el, "getBoundingClientRect", {
|
|
363
|
+
configurable: true,
|
|
364
|
+
value: () => asRect(drawn(live[i])),
|
|
365
|
+
});
|
|
366
|
+
pin();
|
|
367
|
+
pins.push(pin);
|
|
368
|
+
return el;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
frame,
|
|
373
|
+
items,
|
|
374
|
+
laid: live,
|
|
375
|
+
relayout: (next: Box[]) => {
|
|
376
|
+
next.forEach((box, i) => {
|
|
377
|
+
live[i] = { ...box };
|
|
378
|
+
});
|
|
379
|
+
pins.forEach((pin) => pin());
|
|
380
|
+
},
|
|
381
|
+
setFrameDrawn: (next: Box) => {
|
|
382
|
+
frameDrawnBox = next;
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
221
387
|
const scopes: EffectScope[] = [];
|
|
222
388
|
|
|
223
389
|
/** Mount the composable inside a real effect scope (the component case). */
|
|
@@ -874,7 +1040,7 @@ describe("usePointerReorder", () => {
|
|
|
874
1040
|
|
|
875
1041
|
press(handle, items, 0, { x: 10, y: 20 });
|
|
876
1042
|
move(150, 20);
|
|
877
|
-
items.
|
|
1043
|
+
items.shift(); // the strip drops the very chip the pointer is holding
|
|
878
1044
|
move(260, 20);
|
|
879
1045
|
up(260, 20);
|
|
880
1046
|
expect(drops).toEqual([]);
|
|
@@ -886,15 +1052,15 @@ describe("usePointerReorder", () => {
|
|
|
886
1052
|
expect(clicks).toEqual(["row"]);
|
|
887
1053
|
});
|
|
888
1054
|
|
|
889
|
-
it("
|
|
890
|
-
// The caller hands its items over in DISPLAY order,
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
1055
|
+
it("follows the element it grabbed when the strip reorders under it", () => {
|
|
1056
|
+
// The caller hands its items over in DISPLAY order, and the index a drag
|
|
1057
|
+
// landed at is only the way the resolution names the element. A strip
|
|
1058
|
+
// that reorders mid-gesture — a host edit from outside, a row toggled by
|
|
1059
|
+
// a second pointer, a chip's ×, an Enter, a query that filters rows out —
|
|
1060
|
+
// moves the elements under those indices, and the drag has to follow the
|
|
1061
|
+
// ELEMENT: resolving the press-time index would move whichever item slid
|
|
1062
|
+
// into it, and giving way would throw away a gesture the user is still
|
|
1063
|
+
// holding.
|
|
898
1064
|
const items = boxes([
|
|
899
1065
|
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
900
1066
|
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
@@ -907,16 +1073,473 @@ describe("usePointerReorder", () => {
|
|
|
907
1073
|
expect(handle.dragging.value).toBe(true);
|
|
908
1074
|
expect(handle.dragOver.value, "150 is past the first chip's midpoint").toBe(1);
|
|
909
1075
|
|
|
910
|
-
// The
|
|
911
|
-
|
|
1076
|
+
// The host moves the LAST chip to the front: the strip is now
|
|
1077
|
+
// [C, A, B], so the chip the pointer grabbed sits at index 1.
|
|
1078
|
+
const [grabbed, middle, last] = items;
|
|
1079
|
+
const moved = [last, grabbed, middle];
|
|
1080
|
+
moved.forEach((el, i) => reshape(el, { left: i * 100, right: i * 100 + 100, top: 0, bottom: 40 }));
|
|
1081
|
+
items.splice(0, items.length, ...moved);
|
|
912
1082
|
move(260, 20);
|
|
913
|
-
expect(handle.dragging.value, "the drag
|
|
1083
|
+
expect(handle.dragging.value, "the drag is still the one the pointer started").toBe(true);
|
|
1084
|
+
expect(handle.dragFrom.value, "the lift follows the chip it grabbed").toBe(1);
|
|
1085
|
+
expect(handle.dragOver.value, "260 is past the second chip's midpoint of 250").toBe(2);
|
|
1086
|
+
up(260, 20);
|
|
1087
|
+
expect(drops, "the drop is reported in the indices the strip has now").toEqual([[1, 2]]);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it("resolves against where the strip has RE-LAID the held item out", () => {
|
|
1091
|
+
// A font, a zoom or an insertion around the strip re-lays it out under a
|
|
1092
|
+
// live drag, so the item the pointer is holding is no longer where the
|
|
1093
|
+
// press found it. The snapshot describes the old place; the layout
|
|
1094
|
+
// numbers describe the new one, and the resolution has to follow the
|
|
1095
|
+
// strip the user is looking at. The chips here shrink mid-drag, which
|
|
1096
|
+
// brings the held chip's midpoint back behind the pointer: the walk that
|
|
1097
|
+
// the press-time midpoint stopped short of now runs past the strip.
|
|
1098
|
+
const laid: Box[] = [
|
|
1099
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1100
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1101
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1102
|
+
];
|
|
1103
|
+
const strip = laidStrip(laid, { left: 0, top: 0, w: 300, h: 40 }, { left: 0, right: 300, top: 0, bottom: 40 });
|
|
1104
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1105
|
+
|
|
1106
|
+
press(handle, strip.items, 1, { x: 150, y: 20 });
|
|
1107
|
+
move(140, 20);
|
|
1108
|
+
expect(handle.dragging.value).toBe(true);
|
|
1109
|
+
expect(handle.dragOver.value, "the press-time midpoint is still ahead of the pointer").toBe(1);
|
|
1110
|
+
|
|
1111
|
+
// The strip is re-laid out under the drag: every chip, the held one
|
|
1112
|
+
// included, is drawn at its new place.
|
|
1113
|
+
strip.relayout([
|
|
1114
|
+
{ left: 0, right: 50, top: 0, bottom: 40 },
|
|
1115
|
+
{ left: 50, right: 100, top: 0, bottom: 40 },
|
|
1116
|
+
{ left: 100, right: 150, top: 0, bottom: 40 },
|
|
1117
|
+
]);
|
|
1118
|
+
move(140, 20);
|
|
1119
|
+
expect(handle.dragOver.value, "the pointer is past every chip of the new strip").toBe(2);
|
|
1120
|
+
up(140, 20);
|
|
1121
|
+
expect(drops).toEqual([[1, 2]]);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("ignores a transform painted on the held item while it follows the layout", () => {
|
|
1125
|
+
// The whole reason the snapshot exists: a transform on the item — the
|
|
1126
|
+
// drag's own lift, a host animation, a zoomed wrapper — is painted, not
|
|
1127
|
+
// laid out, and must not move the line or the midpoint the resolution
|
|
1128
|
+
// reads. The layout numbers are blind to it, so following the layout
|
|
1129
|
+
// costs nothing here: the same gesture resolves the same way with the
|
|
1130
|
+
// held chip drawn twice as large and shifted.
|
|
1131
|
+
const laid: Box[] = [
|
|
1132
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1133
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1134
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1135
|
+
];
|
|
1136
|
+
const strip = laidStrip(laid, { left: 0, top: 0, w: 300, h: 40 }, { left: 0, right: 300, top: 0, bottom: 40 });
|
|
1137
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1138
|
+
|
|
1139
|
+
press(handle, strip.items, 0, { x: 10, y: 20 });
|
|
1140
|
+
move(150, 20);
|
|
1141
|
+
expect(handle.dragOver.value).toBe(1);
|
|
1142
|
+
|
|
1143
|
+
// A host transform lands on the held chip: an edge-anchored cross-axis
|
|
1144
|
+
// scale, plus a hard translation that carries its drawn box onto the line
|
|
1145
|
+
// below and its drawn midpoint far past where it is laid out — the class
|
|
1146
|
+
// of transform an earlier wave recorded as unfixable while the drawn box
|
|
1147
|
+
// was what got measured. The pointer sits inside the moved band (where
|
|
1148
|
+
// measuring the drawn box makes the held chip the only candidate line, so
|
|
1149
|
+
// the walk stops on its own translated midpoint) and past the second
|
|
1150
|
+
// chip's midpoint, which is where the layout answers "land after the
|
|
1151
|
+
// second chip".
|
|
1152
|
+
reshape(strip.items[0], { left: 100, right: 300, top: 40, bottom: 96 });
|
|
1153
|
+
move(180, 45);
|
|
1154
|
+
expect(handle.dragOver.value, "180 is past the second chip's midpoint of 150").toBe(1);
|
|
1155
|
+
up(180, 45);
|
|
1156
|
+
expect(drops).toEqual([[0, 1]]);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it("carries the derived geometry through a frame that scrolls", () => {
|
|
1160
|
+
// The derivation reads a LAYOUT position and maps it through the box the
|
|
1161
|
+
// frame is drawn in, so the frame's own scroll (which slides the content
|
|
1162
|
+
// inside that box) has to be taken off in the same units. The strip is
|
|
1163
|
+
// scrolled mid-drag; the pointer stays where it is, over a different
|
|
1164
|
+
// chip of the content.
|
|
1165
|
+
const laid: Box[] = [
|
|
1166
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1167
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1168
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1169
|
+
{ left: 300, right: 400, top: 0, bottom: 40 },
|
|
1170
|
+
];
|
|
1171
|
+
const scene = { scale: 1, scroll: 0 };
|
|
1172
|
+
const strip = laidStrip(
|
|
1173
|
+
laid,
|
|
1174
|
+
{ left: 0, top: 0, w: 400, h: 40 },
|
|
1175
|
+
{ left: 0, right: 400, top: 0, bottom: 40 },
|
|
1176
|
+
scene,
|
|
1177
|
+
);
|
|
1178
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1179
|
+
|
|
1180
|
+
press(handle, strip.items, 0, { x: 10, y: 20 });
|
|
1181
|
+
move(40, 20);
|
|
1182
|
+
expect(handle.dragOver.value, "40 is still short of the first chip's midpoint of 50").toBe(0);
|
|
1183
|
+
|
|
1184
|
+
scene.scroll = 120; // the frame scrolls the strip under the drag
|
|
1185
|
+
strip.relayout(laid); // re-pin the drawn boxes at the new scroll
|
|
1186
|
+
move(40, 20);
|
|
1187
|
+
expect(handle.dragOver.value, "the chips moved back under the pointer with the content").toBe(1);
|
|
1188
|
+
up(40, 20);
|
|
1189
|
+
expect(drops).toEqual([[0, 1]]);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it("reads the frame's border the way the frame hands its layout numbers over", () => {
|
|
1193
|
+
// The items' layout numbers are measured from their offsetParent's
|
|
1194
|
+
// PADDING edge — so when the frame is that offsetParent its border has to
|
|
1195
|
+
// be added back, and when the frame is `position: static` (nobody's
|
|
1196
|
+
// offsetParent) the same numbers are already measured from its BORDER
|
|
1197
|
+
// edge and adding it again would count it twice. Both shapes here are
|
|
1198
|
+
// laid out identically and drawn identically: the resolution has to
|
|
1199
|
+
// answer the same for both, and it has to keep the border's own scale.
|
|
1200
|
+
const layout: Box[] = [
|
|
1201
|
+
{ left: 20, right: 60, top: 20, bottom: 60 },
|
|
1202
|
+
{ left: 80, right: 120, top: 20, bottom: 60 },
|
|
1203
|
+
{ left: 140, right: 180, top: 20, bottom: 60 },
|
|
1204
|
+
{ left: 200, right: 240, top: 20, bottom: 60 },
|
|
1205
|
+
];
|
|
1206
|
+
const frameLayout = { left: 0, top: 0, w: 260, h: 80 };
|
|
1207
|
+
const frameDrawn: Box = { left: 0, right: 260, top: 0, bottom: 80 };
|
|
1208
|
+
const border = { left: 20, top: 20 };
|
|
1209
|
+
for (const staticFrame of [false, true]) {
|
|
1210
|
+
const strip = laidStrip(layout, frameLayout, frameDrawn, { border, staticFrame });
|
|
1211
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1212
|
+
press(handle, strip.items, 0, { x: 30, y: 35 });
|
|
1213
|
+
move(102, 62);
|
|
1214
|
+
expect(handle.dragging.value, `staticFrame=${staticFrame}`).toBe(true);
|
|
1215
|
+
expect(handle.dragOver.value, `staticFrame=${staticFrame} lands after the first chip`).toBe(1);
|
|
1216
|
+
up(102, 62);
|
|
1217
|
+
expect(drops, `staticFrame=${staticFrame}`).toEqual([[0, 1]]);
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it("resolves exactly as the held item's own drawn box says, across frame shapes", () => {
|
|
1222
|
+
// The differential form of the test above: for every shape a frame can
|
|
1223
|
+
// have — positioned or static, with or without a border, drawn at its own
|
|
1224
|
+
// size or scaled, scrolled or not, one line or two — the resolution has
|
|
1225
|
+
// to answer what the held item's TRUE layout box answers. The sweep walks
|
|
1226
|
+
// the pointer over each strip and compares against `indexAt` handed that
|
|
1227
|
+
// true box, so any place the derived geometry drifts shows up wherever
|
|
1228
|
+
// the drift can be observed, without hand-picking pointers.
|
|
1229
|
+
const single: Box[] = [
|
|
1230
|
+
{ left: 12, right: 52, top: 12, bottom: 52 },
|
|
1231
|
+
{ left: 72, right: 112, top: 12, bottom: 52 },
|
|
1232
|
+
{ left: 132, right: 172, top: 12, bottom: 52 },
|
|
1233
|
+
];
|
|
1234
|
+
const wrapped: Box[] = [
|
|
1235
|
+
{ left: 12, right: 52, top: 12, bottom: 52 },
|
|
1236
|
+
{ left: 72, right: 112, top: 12, bottom: 52 },
|
|
1237
|
+
{ left: 12, right: 52, top: 72, bottom: 112 },
|
|
1238
|
+
];
|
|
1239
|
+
const scenes = [
|
|
1240
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 0, right: 200, top: 0, bottom: 70 }, scene: {} },
|
|
1241
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 5, right: 235, top: 7, bottom: 112 }, scene: { scale: 1.5 } },
|
|
1242
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 5, right: 235, top: 7, bottom: 112 }, scene: { scale: 1.5, border: { left: 12, top: 12 } } },
|
|
1243
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 5, right: 235, top: 7, bottom: 112 }, scene: { scale: 1.5, border: { left: 12, top: 12 }, staticFrame: true } },
|
|
1244
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 0, right: 200, top: 0, bottom: 70 }, scene: { border: { left: 12, top: 12 } } },
|
|
1245
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 0, right: 200, top: 0, bottom: 70 }, scene: { border: { left: 12, top: 12 }, staticFrame: true } },
|
|
1246
|
+
{ layout: single, frame: { left: 0, top: 0, w: 200, h: 70 }, drawn: { left: 0, right: 200, top: 0, bottom: 70 }, scene: { scroll: 25 } },
|
|
1247
|
+
{ layout: wrapped, frame: { left: 0, top: 0, w: 200, h: 130 }, drawn: { left: 0, right: 200, top: 0, bottom: 130 }, scene: {} },
|
|
1248
|
+
{ layout: wrapped, frame: { left: 0, top: 0, w: 200, h: 130 }, drawn: { left: 0, right: 300, top: 0, bottom: 195 }, scene: { scale: 1.5, border: { left: 8, top: 8 } } },
|
|
1249
|
+
];
|
|
1250
|
+
let compared = 0;
|
|
1251
|
+
for (const [index, entry] of scenes.entries()) {
|
|
1252
|
+
const strip = laidStrip(entry.layout, entry.frame, entry.drawn, entry.scene);
|
|
1253
|
+
const rects = strip.items.map((el) => el.getBoundingClientRect());
|
|
1254
|
+
// The line filter's tolerance is half a pixel wide, so the sweep has to
|
|
1255
|
+
// land ON the band edges, not only near them.
|
|
1256
|
+
const crosses = new Set<number>();
|
|
1257
|
+
for (let cross = -10; cross <= 140; cross += 5) crosses.add(cross);
|
|
1258
|
+
for (const rect of rects) {
|
|
1259
|
+
for (const edge of [rect.top, rect.bottom]) {
|
|
1260
|
+
for (const delta of [-2, -1, -0.5, 0, 0.5, 1, 2]) crosses.add(Number((edge + delta).toFixed(2)));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
for (let from = 0; from < strip.items.length; from += 1) {
|
|
1264
|
+
// The held item's own layout box, drawn — the geometry the resolution
|
|
1265
|
+
// is supposed to be reading.
|
|
1266
|
+
const truth = strip.items[from].getBoundingClientRect();
|
|
1267
|
+
const trueOrigin = {
|
|
1268
|
+
band: { lo: truth.top, hi: truth.bottom },
|
|
1269
|
+
mid: truth.left + truth.width / 2,
|
|
1270
|
+
};
|
|
1271
|
+
for (let main = -20; main <= 240; main += 20) {
|
|
1272
|
+
for (const cross of crosses) {
|
|
1273
|
+
const { handle } = harness("x", strip.items);
|
|
1274
|
+
press(handle, strip.items, from, { x: main - 40, y: cross });
|
|
1275
|
+
move(main, cross);
|
|
1276
|
+
const got = handle.dragOver.value;
|
|
1277
|
+
const want = indexAt(rects, "x", main, cross, from, trueOrigin);
|
|
1278
|
+
expect(got, `scene ${index} from ${from} at (${main}, ${cross})`).toBe(want);
|
|
1279
|
+
up(main, cross);
|
|
1280
|
+
compared += 1;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
expect(compared, "the sweep really ran").toBeGreaterThan(1000);
|
|
1286
|
+
}, 20_000);
|
|
1287
|
+
|
|
1288
|
+
it("keeps the carried snapshot when only half of the box can be read", () => {
|
|
1289
|
+
// A box that reports one side and not the other cannot place anything: a
|
|
1290
|
+
// zero width would put the midpoint on the item's own edge. The gesture
|
|
1291
|
+
// here is one the derivation WOULD answer differently — the strip is
|
|
1292
|
+
// re-laid out under the drag — so the assertion is the carried
|
|
1293
|
+
// snapshot's answer, which is what a strip with no layout numbers at all
|
|
1294
|
+
// gets.
|
|
1295
|
+
const laid: Box[] = [
|
|
1296
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1297
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1298
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1299
|
+
];
|
|
1300
|
+
const strip = laidStrip(laid, { left: 0, top: 0, w: 300, h: 40 }, {
|
|
1301
|
+
left: 0,
|
|
1302
|
+
right: 300,
|
|
1303
|
+
top: 0,
|
|
1304
|
+
bottom: 40,
|
|
1305
|
+
});
|
|
1306
|
+
Object.defineProperty(strip.items[1], "offsetWidth", { configurable: true, get: () => 0 });
|
|
1307
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1308
|
+
|
|
1309
|
+
press(handle, strip.items, 1, { x: 150, y: 20 });
|
|
1310
|
+
move(140, 20);
|
|
1311
|
+
expect(handle.dragging.value).toBe(true);
|
|
1312
|
+
expect(handle.dragOver.value, "the press-time midpoint still stops the walk").toBe(1);
|
|
1313
|
+
|
|
1314
|
+
strip.relayout([
|
|
1315
|
+
{ left: 0, right: 50, top: 0, bottom: 40 },
|
|
1316
|
+
{ left: 50, right: 100, top: 0, bottom: 40 },
|
|
1317
|
+
{ left: 100, right: 150, top: 0, bottom: 40 },
|
|
1318
|
+
]);
|
|
1319
|
+
move(140, 20);
|
|
1320
|
+
expect(handle.dragOver.value, "half a box is no box: the snapshot still resolves").toBe(1);
|
|
1321
|
+
up(140, 20);
|
|
1322
|
+
expect(drops).toEqual([]);
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
it("keeps the carried snapshot when the layout chain cannot be added up", () => {
|
|
1326
|
+
// A chain that runs through something with no offsets of its own — an
|
|
1327
|
+
// element inside `<svg>` reports no `offsetLeft` at all — sums to NaN,
|
|
1328
|
+
// and a NaN origin answers every comparison the same way: it would place
|
|
1329
|
+
// the item at the end of the strip. The chain is checked, and the gesture
|
|
1330
|
+
// falls back to the snapshot.
|
|
1331
|
+
const laid: Box[] = [
|
|
1332
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1333
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1334
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1335
|
+
];
|
|
1336
|
+
const strip = laidStrip(laid, { left: 0, top: 0, w: 300, h: 40 }, {
|
|
1337
|
+
left: 0,
|
|
1338
|
+
right: 300,
|
|
1339
|
+
top: 0,
|
|
1340
|
+
bottom: 40,
|
|
1341
|
+
});
|
|
1342
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1343
|
+
|
|
1344
|
+
press(handle, strip.items, 1, { x: 150, y: 20 });
|
|
1345
|
+
move(140, 20);
|
|
1346
|
+
expect(handle.dragOver.value).toBe(1);
|
|
1347
|
+
|
|
1348
|
+
// The held chip's chain now passes through an element with no offsets.
|
|
1349
|
+
const svgish = document.createElement("div");
|
|
1350
|
+
Object.defineProperty(svgish, "offsetLeft", { configurable: true, get: () => undefined });
|
|
1351
|
+
Object.defineProperty(svgish, "offsetTop", { configurable: true, get: () => undefined });
|
|
1352
|
+
Object.defineProperty(svgish, "offsetParent", {
|
|
1353
|
+
configurable: true,
|
|
1354
|
+
get: () => strip.frame,
|
|
1355
|
+
});
|
|
1356
|
+
Object.defineProperty(strip.items[1], "offsetParent", {
|
|
1357
|
+
configurable: true,
|
|
1358
|
+
get: () => svgish,
|
|
1359
|
+
});
|
|
1360
|
+
strip.relayout([
|
|
1361
|
+
{ left: 0, right: 50, top: 0, bottom: 40 },
|
|
1362
|
+
{ left: 50, right: 100, top: 0, bottom: 40 },
|
|
1363
|
+
{ left: 100, right: 150, top: 0, bottom: 40 },
|
|
1364
|
+
]);
|
|
1365
|
+
move(140, 20);
|
|
1366
|
+
expect(handle.dragOver.value, "an unaddable chain keeps the snapshot's answer").toBe(1);
|
|
1367
|
+
up(140, 20);
|
|
1368
|
+
expect(drops).toEqual([]);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
it("keeps the carried snapshot when the held item collapses", () => {
|
|
1372
|
+
// A held chip re-laid out to no height has no line to be on: its derived
|
|
1373
|
+
// band would be a point and the line filter would drop it, which is a
|
|
1374
|
+
// resolution the layout cannot justify. The fallback answers instead.
|
|
1375
|
+
const laid: Box[] = [
|
|
1376
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1377
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1378
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1379
|
+
];
|
|
1380
|
+
const strip = laidStrip(laid, { left: 0, top: 0, w: 300, h: 40 }, {
|
|
1381
|
+
left: 0,
|
|
1382
|
+
right: 300,
|
|
1383
|
+
top: 0,
|
|
1384
|
+
bottom: 40,
|
|
1385
|
+
});
|
|
1386
|
+
const { handle, drops } = harness("x", strip.items);
|
|
1387
|
+
|
|
1388
|
+
press(handle, strip.items, 1, { x: 150, y: 20 });
|
|
1389
|
+
move(140, 20);
|
|
1390
|
+
expect(handle.dragOver.value).toBe(1);
|
|
1391
|
+
|
|
1392
|
+
strip.relayout([
|
|
1393
|
+
{ left: 0, right: 50, top: 0, bottom: 40 },
|
|
1394
|
+
{ left: 50, right: 100, top: 20, bottom: 20 },
|
|
1395
|
+
{ left: 100, right: 150, top: 0, bottom: 40 },
|
|
1396
|
+
]);
|
|
1397
|
+
move(140, 20);
|
|
1398
|
+
expect(handle.dragOver.value, "a chip with no height has no line to stand on").toBe(1);
|
|
1399
|
+
up(140, 20);
|
|
1400
|
+
expect(drops).toEqual([]);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it("ends the gesture the moment the strip empties", () => {
|
|
1404
|
+
// Nothing left to resolve against is the same answer as the element being
|
|
1405
|
+
// gone: the gesture is over then, not at the release — a drag left
|
|
1406
|
+
// "running" against an empty strip would keep publishing a slot it can no
|
|
1407
|
+
// longer read, and would not swallow the click it owes.
|
|
1408
|
+
const items = boxes([
|
|
1409
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1410
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1411
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1412
|
+
]);
|
|
1413
|
+
const { handle, drops } = harness("x", items);
|
|
1414
|
+
const inner = document.createElement("span");
|
|
1415
|
+
items[0].appendChild(inner);
|
|
1416
|
+
const clicks: string[] = [];
|
|
1417
|
+
inner.addEventListener("click", () => clicks.push("row"));
|
|
1418
|
+
|
|
1419
|
+
press(handle, items, 0, { x: 10, y: 20 });
|
|
1420
|
+
move(150, 20);
|
|
1421
|
+
expect(handle.dragging.value).toBe(true);
|
|
1422
|
+
|
|
1423
|
+
items.splice(0, items.length); // every item goes at once
|
|
1424
|
+
move(260, 20);
|
|
1425
|
+
expect(handle.dragging.value, "the gesture ended with the strip").toBe(false);
|
|
1426
|
+
expect(handle.dragFrom.value).toBe(-1);
|
|
1427
|
+
expect(handle.dragOver.value).toBe(-1);
|
|
1428
|
+
up(260, 20);
|
|
1429
|
+
expect(drops).toEqual([]);
|
|
1430
|
+
inner.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
1431
|
+
expect(clicks, "…and its trailing click was swallowed").toEqual([]);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
it("abandons a drag whose pressed item leaves the strip", () => {
|
|
1435
|
+
// The other side of the same rule: a strip that no longer carries the
|
|
1436
|
+
// element at all — it was removed, or a drag of it can no longer land
|
|
1437
|
+
// anywhere — has nothing for the gesture to move, so it ends without a
|
|
1438
|
+
// reorder.
|
|
1439
|
+
const items = boxes([
|
|
1440
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1441
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1442
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1443
|
+
]);
|
|
1444
|
+
const { handle, drops } = harness("x", items);
|
|
1445
|
+
|
|
1446
|
+
press(handle, items, 0, { x: 10, y: 20 });
|
|
1447
|
+
move(150, 20);
|
|
1448
|
+
expect(handle.dragOver.value).toBe(1);
|
|
1449
|
+
|
|
1450
|
+
items.shift(); // the chip the pointer is holding is gone
|
|
1451
|
+
move(260, 20);
|
|
1452
|
+
expect(handle.dragging.value, "the drag ended with the strip").toBe(false);
|
|
914
1453
|
expect(handle.dragFrom.value).toBe(-1);
|
|
915
1454
|
expect(handle.dragOver.value).toBe(-1);
|
|
916
1455
|
up(260, 20);
|
|
917
1456
|
expect(drops, "nothing was dropped").toEqual([]);
|
|
918
1457
|
});
|
|
919
1458
|
|
|
1459
|
+
it("carries the snapshot exactly through a frame that scrolls AND is scaled", () => {
|
|
1460
|
+
// The frame here is the scroller the strip SITS IN — the shape this
|
|
1461
|
+
// composable documents — and the host zooms it mid-drag while it is
|
|
1462
|
+
// scrolled. The frame's own scroll slides the content inside a box that
|
|
1463
|
+
// does not move, and it does so in the frame's LAYOUT units: taken off
|
|
1464
|
+
// the drawn box as a raw offset it is measured at the wrong magnitude
|
|
1465
|
+
// the moment the frame is scaled (by `(scale - 1) x scroll`, here 300px
|
|
1466
|
+
// on a 2x zoom), and the snapshot drifts off the item it belongs to. The
|
|
1467
|
+
// pointer here is past the third chip and before the fourth, so the
|
|
1468
|
+
// drifted snapshot answers "stay where you are" where the strip answers
|
|
1469
|
+
// "land after the fourth".
|
|
1470
|
+
const { items, scene } = scrollerStrip(5, 100, 40);
|
|
1471
|
+
const { handle, drops } = harness("x", items);
|
|
1472
|
+
|
|
1473
|
+
press(handle, items, 2, { x: 250, y: 20 });
|
|
1474
|
+
scene.scale = 2; // the host zooms the strip…
|
|
1475
|
+
scene.scroll = 300; // …while it is scrolled under the live drag
|
|
1476
|
+
move(100, 20);
|
|
1477
|
+
expect(handle.dragging.value).toBe(true);
|
|
1478
|
+
expect(handle.dragOver.value, "past the third chip, before the fourth").toBe(3);
|
|
1479
|
+
up(100, 20);
|
|
1480
|
+
expect(drops).toEqual([[2, 3]]);
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it("reads a LAYOUT change as a layout change, not as a scale", () => {
|
|
1484
|
+
// The frame's own box growing is not the frame being drawn bigger: the
|
|
1485
|
+
// chips keep their place and their size, and the frame merely got wider
|
|
1486
|
+
// around them. A carrier that reads the drawn box alone cannot tell the
|
|
1487
|
+
// two apart, and would stretch the snapshot by the ratio of the two
|
|
1488
|
+
// sizes — here it would answer "stay where you are" for a pointer the
|
|
1489
|
+
// strip answers "land after the third chip".
|
|
1490
|
+
const { items, scene } = scrollerStrip(5, 100, 40);
|
|
1491
|
+
const { handle, drops } = harness("x", items);
|
|
1492
|
+
|
|
1493
|
+
press(handle, items, 1, { x: 150, y: 20 });
|
|
1494
|
+
scene.laid = 1000; // the frame is re-laid out around the same chips
|
|
1495
|
+
move(260, 20);
|
|
1496
|
+
expect(handle.dragging.value).toBe(true);
|
|
1497
|
+
expect(handle.dragOver.value, "260 is past the third chip's midpoint of 250").toBe(2);
|
|
1498
|
+
up(260, 20);
|
|
1499
|
+
expect(drops).toEqual([[1, 2]]);
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
it("carries the snapshot in the nearest frame that has a box", () => {
|
|
1503
|
+
// A strip wrapped in an element that generates no box of its own
|
|
1504
|
+
// (`display: contents`) is laid out by the next box up, and that is the
|
|
1505
|
+
// box its content moves with: reading the boxless wrapper leaves the
|
|
1506
|
+
// snapshot behind the moment the page scrolls the real frame.
|
|
1507
|
+
const frame = document.createElement("div");
|
|
1508
|
+
const wrapper = document.createElement("div");
|
|
1509
|
+
frame.appendChild(wrapper);
|
|
1510
|
+
document.body.appendChild(frame);
|
|
1511
|
+
const scene = { shift: 0 };
|
|
1512
|
+
reshape(frame, { left: 0, right: 300, top: 0, bottom: 40 });
|
|
1513
|
+
Object.defineProperty(frame, "getBoundingClientRect", {
|
|
1514
|
+
configurable: true,
|
|
1515
|
+
value: () => {
|
|
1516
|
+
const box = { left: 0, right: 300, top: scene.shift, bottom: scene.shift + 40 };
|
|
1517
|
+
return asRect(box);
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
// The wrapper itself: no box at all, exactly as `display: contents` reports.
|
|
1521
|
+
reshape(wrapper, { left: 0, right: 0, top: 0, bottom: 0 });
|
|
1522
|
+
const items = [0, 100, 200].map((start) => {
|
|
1523
|
+
const el = document.createElement("div");
|
|
1524
|
+
Object.defineProperty(el, "getBoundingClientRect", {
|
|
1525
|
+
configurable: true,
|
|
1526
|
+
value: () =>
|
|
1527
|
+
asRect({ left: start, right: start + 100, top: scene.shift, bottom: scene.shift + 40 }),
|
|
1528
|
+
});
|
|
1529
|
+
wrapper.appendChild(el);
|
|
1530
|
+
return el;
|
|
1531
|
+
});
|
|
1532
|
+
const { handle, drops } = harness("x", items);
|
|
1533
|
+
|
|
1534
|
+
press(handle, items, 0, { x: 10, y: 20 });
|
|
1535
|
+
scene.shift = 40; // the page scrolls the strip a line up under the drag
|
|
1536
|
+
move(150, 20);
|
|
1537
|
+
expect(handle.dragging.value).toBe(true);
|
|
1538
|
+
expect(handle.dragOver.value, "the chips kept their places inside the frame").toBe(1);
|
|
1539
|
+
up(150, 20);
|
|
1540
|
+
expect(drops).toEqual([[0, 1]]);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
920
1543
|
it("keeps the pressed item's geometry on the content when the frame is SCALED under it", () => {
|
|
921
1544
|
// A `scale()` or a zoom painted on the frame — or on an ancestor — during
|
|
922
1545
|
// a live drag is not a movement the frame's DISPLACEMENT can describe:
|
|
@@ -75,8 +75,10 @@ export interface PointerReorder {
|
|
|
75
75
|
* crossed the threshold into a drag. The strip is this gesture's until
|
|
76
76
|
* it ends, so a caller that can change the order by other means (the
|
|
77
77
|
* panel's own keyboard reorder, a host edit) can hold off while this is
|
|
78
|
-
* `true`: the drag
|
|
79
|
-
* is
|
|
78
|
+
* `true`: the drag moves the element the press grabbed and the snapshot
|
|
79
|
+
* it resolves by is the one the press read, so an order that reorders the
|
|
80
|
+
* strip under the gesture is an order resolved against geometry its item
|
|
81
|
+
* no longer occupies. */
|
|
80
82
|
pressed: Ref<boolean>;
|
|
81
83
|
/** Watch a press on item `index` (item 0 of `items()` is index 0).
|
|
82
84
|
* Nothing is reported until the pointer travels past the threshold,
|
|
@@ -191,7 +193,13 @@ export function indexAt(
|
|
|
191
193
|
* before the lift is painted on it (`start`) and carried along with the
|
|
192
194
|
* frame it was read in, however that frame moves under the drag — so no
|
|
193
195
|
* transform an item picks up mid-drag can move the line it is dragged along
|
|
194
|
-
* or the midpoint it is compared against.
|
|
196
|
+
* or the midpoint it is compared against. The drag is moved BY ELEMENT: the
|
|
197
|
+
* press records the element it landed on and every resolution re-reads that
|
|
198
|
+
* element's index from the live strip, so a list that reorders under the
|
|
199
|
+
* gesture (a host edit, a second pointer, a removal elsewhere) keeps the
|
|
200
|
+
* drop on what the pointer is holding instead of on whatever slid into the
|
|
201
|
+
* index it started at — and a gesture whose element is gone from the strip
|
|
202
|
+
* ends without a reorder.
|
|
195
203
|
*
|
|
196
204
|
* The threshold is what keeps the gesture honest on a control that is also
|
|
197
205
|
* clickable: a press under ~6px is not a drag at all — no state is
|
|
@@ -236,15 +244,19 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
236
244
|
let lastMain = 0;
|
|
237
245
|
let lastCross = 0;
|
|
238
246
|
/** The pressed item's LAYOUT geometry (`null` until a press reads it), the
|
|
239
|
-
* FRAME it was laid out in (its parent element) and
|
|
240
|
-
* when the geometry was
|
|
241
|
-
* instead of the item's live rect, which carries the lift.
|
|
247
|
+
* FRAME it was laid out in (its parent element) and that frame's reading
|
|
248
|
+
* when the geometry was taken — the whole drag resolves against this
|
|
249
|
+
* instead of the item's live rect, which carries the lift. The geometry
|
|
250
|
+
* stays the press's even if the ELEMENT is re-anchored to another index
|
|
251
|
+
* mid-gesture (`slotAt`): the pointer grabbed it where it was. */
|
|
242
252
|
let pressedBand: { lo: number; hi: number } | null = null;
|
|
243
253
|
let pressedMid = 0;
|
|
244
254
|
let pressedFrame: HTMLElement | null = null;
|
|
245
|
-
let pressedFrameBox:
|
|
246
|
-
/** The very ELEMENT the press landed on
|
|
247
|
-
* is
|
|
255
|
+
let pressedFrameBox: FrameBox | null = null;
|
|
256
|
+
/** The very ELEMENT the press landed on. The drag is moved BY element:
|
|
257
|
+
* the index is re-read from it on every resolution, so a strip that
|
|
258
|
+
* reorders under the gesture keeps the drop on what the pointer grabbed
|
|
259
|
+
* (`slotAt`). */
|
|
248
260
|
let pressedItem: HTMLElement | null = null;
|
|
249
261
|
/** Auto-scroll: px per frame (signed: negative scrolls back), 0 = off. */
|
|
250
262
|
let scrollVelocity = 0;
|
|
@@ -262,87 +274,219 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
262
274
|
return options.items().filter((el): el is HTMLElement => el != null);
|
|
263
275
|
}
|
|
264
276
|
|
|
265
|
-
/** The slot the live strip resolves for a pointer at (`main`, `cross`)
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* first: that the
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
|
|
277
|
+
/** The slot the live strip resolves for a pointer at (`main`, `cross`).
|
|
278
|
+
* The press is on an ELEMENT, and the index it landed at is only the way
|
|
279
|
+
* the resolution names it — so the index is re-read from the element
|
|
280
|
+
* first: anything that reorders the strip under the gesture (a host edit,
|
|
281
|
+
* a row toggled by a second pointer, a chip's ×, an Enter, a query that
|
|
282
|
+
* filters rows out) moves the elements under those indices, while the
|
|
283
|
+
* element the pointer grabbed is still the one being dragged. The
|
|
284
|
+
* resolution then follows IT, and the caller's `onDrop` gets the indices
|
|
285
|
+
* the strip has now. Only a strip that no longer carries the element at
|
|
286
|
+
* all — it was removed, or a drag of it can no longer land anywhere —
|
|
287
|
+
* ends the gesture, and it ends without a reorder. */
|
|
288
|
+
function slotAt(main: number, cross: number): number {
|
|
275
289
|
const items = liveItems();
|
|
276
|
-
if (items.length === 0)
|
|
277
|
-
if (pressedItem && items[pressedIndex] !== pressedItem) {
|
|
290
|
+
if (items.length === 0) {
|
|
278
291
|
abandonDrag();
|
|
279
292
|
return -1;
|
|
280
293
|
}
|
|
294
|
+
if (pressedItem) {
|
|
295
|
+
const index = items.indexOf(pressedItem);
|
|
296
|
+
if (index < 0) {
|
|
297
|
+
abandonDrag();
|
|
298
|
+
return -1;
|
|
299
|
+
}
|
|
300
|
+
if (index !== pressedIndex) {
|
|
301
|
+
pressedIndex = index;
|
|
302
|
+
if (dragging.value) dragFrom.value = index;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
281
305
|
return indexAt(
|
|
282
306
|
items.map((item) => item.getBoundingClientRect()),
|
|
283
307
|
axis,
|
|
284
308
|
main,
|
|
285
309
|
cross,
|
|
286
|
-
|
|
310
|
+
pressedIndex,
|
|
287
311
|
layoutOrigin(),
|
|
288
312
|
);
|
|
289
313
|
}
|
|
290
314
|
|
|
291
|
-
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* the
|
|
296
|
-
*
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Everything the frame says about where it puts a coordinate: the box the
|
|
317
|
+
* item is laid out in — where it is drawn (`x`/`y`), how big it is drawn
|
|
318
|
+
* (`w`/`h`) and how big it is LAID OUT (`laidW`/`laidH`, which no
|
|
319
|
+
* transform touches, so the two together are the scale it is drawn at) —
|
|
320
|
+
* and the scroll that slides its children inside that box.
|
|
321
|
+
*/
|
|
322
|
+
interface FrameBox {
|
|
323
|
+
x: number;
|
|
324
|
+
y: number;
|
|
325
|
+
w: number;
|
|
326
|
+
h: number;
|
|
327
|
+
laidW: number;
|
|
328
|
+
laidH: number;
|
|
329
|
+
scrollX: number;
|
|
330
|
+
scrollY: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The nearest ancestor of `item` that is actually LAID OUT. A snapshot
|
|
334
|
+
* can only be carried in a frame that has a box to move it: an element
|
|
335
|
+
* with `display: contents` generates none, and its children are laid out
|
|
336
|
+
* by the next box up — which is the frame whose movement they follow. A
|
|
337
|
+
* tree with no boxes anywhere (no layout engine at all, a test
|
|
338
|
+
* environment) walks out of ancestors and keeps the parent it started
|
|
339
|
+
* with. */
|
|
340
|
+
function layoutFrameOf(item: HTMLElement): HTMLElement | null {
|
|
341
|
+
for (let el = item.parentElement; el; el = el.parentElement) {
|
|
342
|
+
const rect = el.getBoundingClientRect();
|
|
343
|
+
if (rect.width > 0 || rect.height > 0 || el.offsetWidth > 0 || el.offsetHeight > 0) return el;
|
|
344
|
+
}
|
|
345
|
+
return item.parentElement;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Read the frame as it is right now (see `FrameBox`). */
|
|
349
|
+
function frameBox(frame: HTMLElement): FrameBox {
|
|
307
350
|
const rect = frame.getBoundingClientRect();
|
|
308
351
|
return {
|
|
309
|
-
x: rect.left
|
|
310
|
-
y: rect.top
|
|
352
|
+
x: rect.left,
|
|
353
|
+
y: rect.top,
|
|
311
354
|
w: rect.width,
|
|
312
355
|
h: rect.height,
|
|
356
|
+
laidW: frame.offsetWidth,
|
|
357
|
+
laidH: frame.offsetHeight,
|
|
358
|
+
scrollX: frame.scrollLeft,
|
|
359
|
+
scrollY: frame.scrollTop,
|
|
313
360
|
};
|
|
314
361
|
}
|
|
315
362
|
|
|
363
|
+
/** Where an element's border box sits in a space its ancestors share: the
|
|
364
|
+
* one number CSS transforms do not touch. `offsetLeft`/`offsetTop` are
|
|
365
|
+
* LAYOUT positions — the drag's own lift, a host `scale()`, a FLIP
|
|
366
|
+
* animation and a transition mid-flight all leave them exactly where they
|
|
367
|
+
* were — so walking the offsetParent chain of the item and of its frame
|
|
368
|
+
* and subtracting gives the item's current place inside the frame however
|
|
369
|
+
* it is drawn. Both sides are walked to the same root, so a frame that is
|
|
370
|
+
* not itself an offsetParent (a static frame) cancels exactly.
|
|
371
|
+
*
|
|
372
|
+
* `through` reports whether the walk passed THROUGH the frame, which is
|
|
373
|
+
* what decides how the two differ: an offset is measured from the
|
|
374
|
+
* offsetParent's PADDING edge, so a chain that runs through the frame
|
|
375
|
+
* leaves the difference measured from the frame's padding box (its border
|
|
376
|
+
* has to be added back), while a chain that skips it — a frame that is
|
|
377
|
+
* `position: static` is nobody's offsetParent — leaves the difference
|
|
378
|
+
* measured from the frame's BORDER box, where adding the border again
|
|
379
|
+
* would double-count it.
|
|
380
|
+
*
|
|
381
|
+
* The chain must be walked LIVE: a transform (or even
|
|
382
|
+
* `will-change: transform`) on an ancestor rebinds `offsetParent`. */
|
|
383
|
+
function layoutOffset(
|
|
384
|
+
el: HTMLElement,
|
|
385
|
+
frame: HTMLElement | null,
|
|
386
|
+
): { x: number; y: number; through: boolean } {
|
|
387
|
+
let x = 0;
|
|
388
|
+
let y = 0;
|
|
389
|
+
let through = false;
|
|
390
|
+
for (let node: HTMLElement | null = el; node; node = node.offsetParent as HTMLElement | null) {
|
|
391
|
+
x += node.offsetLeft;
|
|
392
|
+
y += node.offsetTop;
|
|
393
|
+
if (node === frame) through = true;
|
|
394
|
+
}
|
|
395
|
+
return { x, y, through };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The pressed item's layout geometry read off the LIVE strip rather than
|
|
399
|
+
* carried from the press — the answer to "where is the item I am holding
|
|
400
|
+
* NOW?", which is what changes when the list reorders, an item is added or
|
|
401
|
+
* removed around it, a font or a zoom changes, or the strip re-wraps.
|
|
402
|
+
* `null` when there is no box to read (no layout engine at all, a detached
|
|
403
|
+
* element, a collapsed or partially-readable one), which keeps the caller
|
|
404
|
+
* on the carried snapshot. */
|
|
405
|
+
function derivedOrigin(): PointerReorderOrigin | null {
|
|
406
|
+
if (!pressedItem || !pressedFrame || !pressedFrame.isConnected) return null;
|
|
407
|
+
const box = frameBox(pressedFrame);
|
|
408
|
+
const itemW = pressedItem.offsetWidth;
|
|
409
|
+
const itemH = pressedItem.offsetHeight;
|
|
410
|
+
if (!(itemW > 0) || !(itemH > 0)) return null;
|
|
411
|
+
if (!(box.w > 0) || !(box.h > 0)) return null;
|
|
412
|
+
const at = layoutOffset(pressedItem, pressedFrame);
|
|
413
|
+
const of = layoutOffset(pressedFrame, null);
|
|
414
|
+
// A chain that runs through something without layout offsets of its own
|
|
415
|
+
// (an SVG ancestor, a foreignObject boundary) sums to NaN rather than
|
|
416
|
+
// failing: nothing can be placed from it, so the snapshot keeps the call.
|
|
417
|
+
if (!Number.isFinite(at.x) || !Number.isFinite(at.y)) return null;
|
|
418
|
+
if (!Number.isFinite(of.x) || !Number.isFinite(of.y)) return null;
|
|
419
|
+
const scaleX = box.laidW > 0 ? box.w / box.laidW : 1;
|
|
420
|
+
const scaleY = box.laidH > 0 ? box.h / box.laidH : 1;
|
|
421
|
+
/** The frame's own border, in the units it is DRAWN at — and only when
|
|
422
|
+
* the walk into the frame came through it (`through`). */
|
|
423
|
+
const style = getComputedStyle(pressedFrame);
|
|
424
|
+
const borderLeft = at.through ? (parseFloat(style.borderLeftWidth) || 0) * scaleX : 0;
|
|
425
|
+
const borderTop = at.through ? (parseFloat(style.borderTopWidth) || 0) * scaleY : 0;
|
|
426
|
+
const left = box.x + borderLeft + (at.x - of.x - box.scrollX) * scaleX;
|
|
427
|
+
const top = box.y + borderTop + (at.y - of.y - box.scrollY) * scaleY;
|
|
428
|
+
const width = itemW * scaleX;
|
|
429
|
+
const height = itemH * scaleY;
|
|
430
|
+
return axis === "x"
|
|
431
|
+
? { band: { lo: top, hi: top + height }, mid: left + width / 2 }
|
|
432
|
+
: { band: { lo: left, hi: left + width }, mid: top + height / 2 };
|
|
433
|
+
}
|
|
316
434
|
|
|
317
|
-
/** The pressed item's layout geometry for `indexAt`, placed
|
|
318
|
-
*
|
|
435
|
+
/** The pressed item's layout geometry for `indexAt`, placed where the
|
|
436
|
+
* frame puts it NOW. The snapshot is in VIEWPORT coordinates, so anything
|
|
319
437
|
* that moves the frame under it — the auto-scroll this composable drives,
|
|
320
438
|
* a host scrolling the surface or any scroller above it, a page scroll, a
|
|
321
|
-
* re-parent, a transform on an ancestor — has to be
|
|
322
|
-
* the line the item was laid out on would be compared
|
|
323
|
-
* that have since moved away from it. `null` before any
|
|
324
|
-
* press whose item was not in the strip.
|
|
439
|
+
* re-parent, a transform on the frame or on an ancestor — has to be
|
|
440
|
+
* carried with it, or the line the item was laid out on would be compared
|
|
441
|
+
* against siblings that have since moved away from it. `null` before any
|
|
442
|
+
* press, and for a press whose item was not in the strip.
|
|
325
443
|
*
|
|
326
|
-
* The carrier is the frame's
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
444
|
+
* The carrier is the frame's own reading of a coordinate: the snapshot
|
|
445
|
+
* travels to wherever the frame puts that same place in its layout now,
|
|
446
|
+
* which is exact for the three things that can move it —
|
|
447
|
+
*
|
|
448
|
+
* - a DISPLACEMENT of the frame (a page scroll, a re-parent, a
|
|
449
|
+
* translation) moves the snapshot by the same amount;
|
|
450
|
+
* - the frame's own SCROLL slides its children inside a box that does not
|
|
451
|
+
* move, so it is taken off inside the frame's own units rather than in
|
|
452
|
+
* drawn pixels (`sample`);
|
|
453
|
+
* - a `scale()` or a zoom — on the frame or on an ancestor — keeps the
|
|
454
|
+
* snapshot at the same place INSIDE the frame, which is what the frame
|
|
455
|
+
* does to everything it holds, so the placement scales with it.
|
|
456
|
+
*
|
|
457
|
+
* A frame with no box to read any of this from falls back to the plain
|
|
333
458
|
* displacement. */
|
|
334
459
|
function layoutOrigin(): PointerReorderOrigin | null {
|
|
460
|
+
const live = derivedOrigin();
|
|
461
|
+
if (live) return live;
|
|
335
462
|
if (!pressedBand || !pressedFrameBox) return null;
|
|
336
463
|
const at = pressedFrameBox;
|
|
337
464
|
const now = pressedFrame && pressedFrame.isConnected ? frameBox(pressedFrame) : at;
|
|
465
|
+
/** Where the frame draws a coordinate it drew at `from` when the press
|
|
466
|
+
* was read: how far it sat from the frame's content origin, taken
|
|
467
|
+
* against the scroll the frame has moved since, drawn again at the
|
|
468
|
+
* scale the frame carries now. A frame whose LAID-OUT size cannot be
|
|
469
|
+
* read (no layout engine, `display: contents`) answers with the ratio
|
|
470
|
+
* of its drawn sizes, and takes its own scroll off raw — the reading
|
|
471
|
+
* every frame got before the scale was measurable at all. A frame with
|
|
472
|
+
* no box and no scroll of its own — one that cannot be read at all —
|
|
473
|
+
* carries nothing, so the snapshot stays where the press put it. */
|
|
338
474
|
const carry = (value: number, along: "x" | "y"): number => {
|
|
339
475
|
const from = along === "x" ? at.x : at.y;
|
|
340
|
-
const fromSpan = along === "x" ? at.w : at.h;
|
|
341
476
|
const to = along === "x" ? now.x : now.y;
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
477
|
+
const drawnFrom = along === "x" ? at.w : at.h;
|
|
478
|
+
const drawnTo = along === "x" ? now.w : now.h;
|
|
479
|
+
const laidFrom = along === "x" ? at.laidW : at.laidH;
|
|
480
|
+
const laidTo = along === "x" ? now.laidW : now.laidH;
|
|
481
|
+
const scrollFrom = along === "x" ? at.scrollX : at.scrollY;
|
|
482
|
+
const scrollTo = along === "x" ? now.scrollX : now.scrollY;
|
|
483
|
+
if (laidFrom > 0 && laidTo > 0) {
|
|
484
|
+
const scaleFrom = drawnFrom / laidFrom;
|
|
485
|
+
const scaleTo = drawnTo / laidTo;
|
|
486
|
+
return to + (scaleTo / scaleFrom) * (value - from) + scaleTo * (scrollFrom - scrollTo);
|
|
487
|
+
}
|
|
488
|
+
const ratio = drawnFrom > 0 && drawnTo > 0 ? drawnTo / drawnFrom : 1;
|
|
489
|
+
return to + ratio * (value - from) + (scrollFrom - scrollTo);
|
|
346
490
|
};
|
|
347
491
|
const down = axis === "x" ? "y" : "x";
|
|
348
492
|
const lo = carry(pressedBand.lo, down);
|
|
@@ -403,7 +547,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
403
547
|
} else {
|
|
404
548
|
container.scrollTop = next;
|
|
405
549
|
}
|
|
406
|
-
const over = slotAt(lastMain, lastCross
|
|
550
|
+
const over = slotAt(lastMain, lastCross);
|
|
407
551
|
if (over >= 0) dragOver.value = over;
|
|
408
552
|
scrollFrameId = requestAnimationFrame(scrollTick);
|
|
409
553
|
}
|
|
@@ -527,16 +671,18 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
527
671
|
const at = pointerAt(event);
|
|
528
672
|
lastMain = at.main;
|
|
529
673
|
lastCross = at.cross;
|
|
530
|
-
const over = slotAt(lastMain, lastCross
|
|
674
|
+
const over = slotAt(lastMain, lastCross);
|
|
531
675
|
if (over >= 0) dragOver.value = over;
|
|
532
676
|
syncAutoScroll();
|
|
533
677
|
}
|
|
534
678
|
|
|
535
679
|
function onPointerUp(event: PointerEvent): void {
|
|
536
680
|
if (!pressed.value || event.pointerId !== pointerId) return;
|
|
537
|
-
const from = pressedIndex;
|
|
538
681
|
const at = pointerAt(event);
|
|
539
|
-
|
|
682
|
+
// The slot is resolved first: it re-reads the pressed ELEMENT's index
|
|
683
|
+
// from the live strip, and that is the index the drop is reported with.
|
|
684
|
+
const to = moved ? slotAt(at.main, at.cross) : pressedIndex;
|
|
685
|
+
const from = pressedIndex;
|
|
540
686
|
const dropped = moved && to >= 0 && to !== from;
|
|
541
687
|
// The drag is over either way — the trap only guards the click the
|
|
542
688
|
// browser is about to deliver.
|
|
@@ -545,11 +691,12 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
545
691
|
if (dropped) options.onDrop(from, to);
|
|
546
692
|
}
|
|
547
693
|
|
|
548
|
-
/** End a drag that must not resolve — the strip
|
|
549
|
-
*
|
|
550
|
-
* exactly as a drop's is: this WAS a drag
|
|
551
|
-
* threshold), and the click the browser is
|
|
552
|
-
* the finger came to rest is one the user
|
|
694
|
+
/** End a drag that must not resolve — the strip no longer carries the
|
|
695
|
+
* element the press grabbed, so there is nothing for the gesture to move.
|
|
696
|
+
* The trailing click is swallowed exactly as a drop's is: this WAS a drag
|
|
697
|
+
* (the pointer travelled past the threshold), and the click the browser is
|
|
698
|
+
* about to deliver at wherever the finger came to rest is one the user
|
|
699
|
+
* never made. */
|
|
553
700
|
function abandonDrag(): void {
|
|
554
701
|
if (moved) trapClick();
|
|
555
702
|
releaseDrag();
|
|
@@ -594,7 +741,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
594
741
|
? rect.left + rect.width / 2
|
|
595
742
|
: rect.top + rect.height / 2
|
|
596
743
|
: 0;
|
|
597
|
-
pressedFrame = item
|
|
744
|
+
pressedFrame = item ? layoutFrameOf(item) : null;
|
|
598
745
|
pressedFrameBox = pressedFrame ? frameBox(pressedFrame) : null;
|
|
599
746
|
pressedItem = item ?? null;
|
|
600
747
|
originX = event.clientX;
|