@celestia-island/hikari 0.43.7 → 0.43.9
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
|
@@ -329,10 +329,12 @@
|
|
|
329
329
|
* happened. The dimming stays LIGHT on purpose (0.85, the value of the
|
|
330
330
|
* HkDraggableList/HkDraggableGrid drag ghost): the item must still read as
|
|
331
331
|
* the one being moved, not fade out. `transform` reflows nothing — no
|
|
332
|
-
* neighbour ever moves for it — and
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
332
|
+
* neighbour ever moves for it — and it cannot move the drag's own geometry
|
|
333
|
+
* either: the resolution reads the pressed item's line and midpoint from
|
|
334
|
+
* the layout captured when the press landed, before this lift is painted.
|
|
335
|
+
* The raised shadow replaces the row's own box-shadow faces for as long as
|
|
336
|
+
* the drag lasts (the keyboard cursor's inset ring included: the lift is
|
|
337
|
+
* the live state).
|
|
336
338
|
*
|
|
337
339
|
* Chips carry the state as classes (HkTag takes no arbitrary attributes),
|
|
338
340
|
* panel rows as data attributes — both are assertable hooks that never
|
|
@@ -348,16 +350,17 @@
|
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
/* The scale runs ALONG each list's own drag axis — chips on X (a horizontal
|
|
351
|
-
* strip), rows on Y (a vertical list)
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* rects (usePointerReorder's `indexAt`)
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
353
|
+
* strip), rows on Y (a vertical list): a small scale that follows the
|
|
354
|
+
* gesture keeps the lifted item tight in a dense chip row. WHICH axis it
|
|
355
|
+
* runs on is TASTE, not necessity — a drag resolves its landing slot from
|
|
356
|
+
* the items' live rects (usePointerReorder's `indexAt`), where every item's
|
|
357
|
+
* span on the axis ACROSS the drag decides which line the pointer is on and
|
|
358
|
+
* the item's MIDPOINT on the drag axis picks the slot, except that the item
|
|
359
|
+
* being dragged is measured by the geometry captured when the press landed,
|
|
360
|
+
* before this lift is painted: no transform on it, along the axis, across it
|
|
361
|
+
* or an isotropic `scale()`, can move the line it is dragged along. The line
|
|
362
|
+
* filter started out a no-op precisely because every row spans the list's
|
|
363
|
+
* full width. */
|
|
361
364
|
.hk-tag-input-tag-dragging {
|
|
362
365
|
transform: scaleX(1.03);
|
|
363
366
|
}
|
|
@@ -53,13 +53,16 @@ function mountTagInput(opts: MountOptions = {}) {
|
|
|
53
53
|
};
|
|
54
54
|
const container = document.createElement("div");
|
|
55
55
|
document.body.appendChild(container);
|
|
56
|
+
/** The host's own array — the component's `modelValue` comes straight
|
|
57
|
+
* from it, so a test can rewrite the order out of band (a host edit
|
|
58
|
+
* landing mid-gesture) instead of only through the component's emits. */
|
|
59
|
+
const model = ref<readonly string[]>(opts.modelValue ?? []);
|
|
56
60
|
const Host = defineComponent({
|
|
57
61
|
name: "HkTagInputHost",
|
|
58
62
|
setup() {
|
|
59
|
-
const value = ref<readonly string[]>(opts.modelValue ?? []);
|
|
60
63
|
return () =>
|
|
61
64
|
h(HkTagInput, {
|
|
62
|
-
modelValue:
|
|
65
|
+
modelValue: model.value,
|
|
63
66
|
options: opts.options ?? OPTIONS,
|
|
64
67
|
allowCustom: opts.allowCustom ?? false,
|
|
65
68
|
label: opts.label,
|
|
@@ -72,7 +75,7 @@ function mountTagInput(opts: MountOptions = {}) {
|
|
|
72
75
|
emptyText: opts.emptyText,
|
|
73
76
|
"onUpdate:modelValue": (keys: string[]) => {
|
|
74
77
|
events.modelValue.push(keys);
|
|
75
|
-
|
|
78
|
+
model.value = keys;
|
|
76
79
|
},
|
|
77
80
|
onAdd: (key: string) => events.add.push(key),
|
|
78
81
|
onRemove: (key: string) => events.remove.push(key),
|
|
@@ -83,7 +86,7 @@ function mountTagInput(opts: MountOptions = {}) {
|
|
|
83
86
|
const app = createApp(Host);
|
|
84
87
|
app.mount(container);
|
|
85
88
|
mounts.push({ app, container });
|
|
86
|
-
return { events, container };
|
|
89
|
+
return { events, container, model };
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
async function settle(): Promise<void> {
|
|
@@ -1192,6 +1195,15 @@ function holdPointer(
|
|
|
1192
1195
|
clientY: from.y,
|
|
1193
1196
|
}),
|
|
1194
1197
|
);
|
|
1198
|
+
movePointer(to, pointerType);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** Move an already-held pointer — a second step of a gesture whose press
|
|
1202
|
+
* (and its exact travel) the test wants to control. */
|
|
1203
|
+
function movePointer(
|
|
1204
|
+
to: { x: number; y: number },
|
|
1205
|
+
pointerType: "mouse" | "touch" = "mouse",
|
|
1206
|
+
): void {
|
|
1195
1207
|
window.dispatchEvent(
|
|
1196
1208
|
new PointerEvent("pointermove", {
|
|
1197
1209
|
bubbles: true,
|
|
@@ -1434,6 +1446,156 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1434
1446
|
expect(rowLabels().slice(0, 3)).toEqual(["Technology", "Germany", "News"]);
|
|
1435
1447
|
});
|
|
1436
1448
|
|
|
1449
|
+
it("leaves the order alone when a keyboard nudge lands under a held row drag", async () => {
|
|
1450
|
+
const { events, container } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1451
|
+
await openPanel(container);
|
|
1452
|
+
const selected = reorderableRows();
|
|
1453
|
+
pinStrip(selected, 40, "y");
|
|
1454
|
+
const search = searchInput();
|
|
1455
|
+
search.focus();
|
|
1456
|
+
pressKey(search, "ArrowDown");
|
|
1457
|
+
await settle();
|
|
1458
|
+
pressKey(search, "ArrowDown");
|
|
1459
|
+
await settle();
|
|
1460
|
+
expect(activeRow()).toBe(rowByLabel("Technology"));
|
|
1461
|
+
|
|
1462
|
+
// A drag on the FIRST row is held while the cursor's row is nudged with
|
|
1463
|
+
// Alt+ArrowUp. The drag's landing slot is an index into the strip it
|
|
1464
|
+
// grabbed, so a reorder under the held pointer would leave the release
|
|
1465
|
+
// moving whatever now sits at that index — a different row, silently.
|
|
1466
|
+
// The chord is still consumed (Alt+Arrow is not a plain arrow), it just
|
|
1467
|
+
// has nothing to move while a pointer owns the order.
|
|
1468
|
+
holdPointer({ x: 10, y: 10 }, { x: 10, y: 130 }, grip(selected[0]));
|
|
1469
|
+
await settle();
|
|
1470
|
+
pressKey(search, "ArrowUp", { altKey: true });
|
|
1471
|
+
await settle();
|
|
1472
|
+
expect(events.modelValue, "the chord emits nothing mid-drag").toEqual([]);
|
|
1473
|
+
expect(rowLabels().slice(0, 3), "the rows stayed where they were").toEqual([
|
|
1474
|
+
"News",
|
|
1475
|
+
"Technology",
|
|
1476
|
+
"Germany",
|
|
1477
|
+
]);
|
|
1478
|
+
releasePointer({ x: 10, y: 130 });
|
|
1479
|
+
await settle();
|
|
1480
|
+
|
|
1481
|
+
// …and the drop moves the row the pointer actually grabbed.
|
|
1482
|
+
expect(events.modelValue.at(-1)).toEqual(["tech", "de", "news"]);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it("keeps the chord out of a press that has not become a drag yet", async () => {
|
|
1486
|
+
const { events, container } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1487
|
+
await openPanel(container);
|
|
1488
|
+
const selected = reorderableRows();
|
|
1489
|
+
pinStrip(selected, 40, "y");
|
|
1490
|
+
const search = searchInput();
|
|
1491
|
+
search.focus();
|
|
1492
|
+
pressKey(search, "ArrowDown");
|
|
1493
|
+
await settle();
|
|
1494
|
+
pressKey(search, "ArrowDown");
|
|
1495
|
+
await settle();
|
|
1496
|
+
expect(activeRow()).toBe(rowByLabel("Technology"));
|
|
1497
|
+
|
|
1498
|
+
// The same chord, one step EARLIER: the pointer is down on the first
|
|
1499
|
+
// row's grip but has not travelled the few pixels that turn a press into
|
|
1500
|
+
// a drag, so nothing has been published yet. The press already owns the
|
|
1501
|
+
// strip, though — crossing the threshold after the chord had moved a row
|
|
1502
|
+
// would leave the drop resolving against a strip it never grabbed.
|
|
1503
|
+
holdPointer({ x: 10, y: 10 }, { x: 12, y: 11 }, grip(selected[0]));
|
|
1504
|
+
await settle();
|
|
1505
|
+
pressKey(search, "ArrowUp", { altKey: true });
|
|
1506
|
+
await settle();
|
|
1507
|
+
expect(events.modelValue, "the chord emits nothing under a live press").toEqual([]);
|
|
1508
|
+
|
|
1509
|
+
// Crossing the threshold now still moves the row the pointer grabbed.
|
|
1510
|
+
movePointer({ x: 10, y: 130 });
|
|
1511
|
+
releasePointer({ x: 10, y: 130 });
|
|
1512
|
+
await settle();
|
|
1513
|
+
expect(events.modelValue.at(-1)).toEqual(["tech", "de", "news"]);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
it("keeps the chord out of a live CHIP drag too", async () => {
|
|
1517
|
+
const { events, container } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1518
|
+
await openPanel(container);
|
|
1519
|
+
const strip = tags(container);
|
|
1520
|
+
pinStrip(strip, 60, "x");
|
|
1521
|
+
const search = searchInput();
|
|
1522
|
+
search.focus();
|
|
1523
|
+
pressKey(search, "ArrowDown");
|
|
1524
|
+
await settle();
|
|
1525
|
+
pressKey(search, "ArrowDown");
|
|
1526
|
+
await settle();
|
|
1527
|
+
expect(activeRow()).toBe(rowByLabel("Technology"));
|
|
1528
|
+
|
|
1529
|
+
// The panel rows and the field's chips are two views of ONE array, so a
|
|
1530
|
+
// chord that reorders the rows edits the strip a chip drag is resolving
|
|
1531
|
+
// against — the drag has to hold the order whichever list it grabbed.
|
|
1532
|
+
holdPointer({ x: 10, y: 10 }, { x: 200, y: 10 }, strip[0]);
|
|
1533
|
+
await settle();
|
|
1534
|
+
pressKey(search, "ArrowUp", { altKey: true });
|
|
1535
|
+
await settle();
|
|
1536
|
+
expect(events.modelValue, "the chord emits nothing mid-drag").toEqual([]);
|
|
1537
|
+
expect(tagTexts(container), "the chips stayed where they were").toEqual([
|
|
1538
|
+
"News",
|
|
1539
|
+
"Technology",
|
|
1540
|
+
"Germany",
|
|
1541
|
+
]);
|
|
1542
|
+
releasePointer({ x: 200, y: 10 });
|
|
1543
|
+
await settle();
|
|
1544
|
+
|
|
1545
|
+
// The chip the pointer grabbed is the one that moved.
|
|
1546
|
+
expect(events.modelValue.at(-1)).toEqual(["tech", "de", "news"]);
|
|
1547
|
+
expect(tagTexts(container)).toEqual(["Technology", "Germany", "News"]);
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it("follows the row it grabbed when the host reorders the list under it", async () => {
|
|
1551
|
+
const { events, container, model } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1552
|
+
await openPanel(container);
|
|
1553
|
+
const selected = reorderableRows();
|
|
1554
|
+
pinStrip(selected, 40, "y");
|
|
1555
|
+
|
|
1556
|
+
holdPointer({ x: 10, y: 10 }, { x: 10, y: 130 }, grip(selected[0]));
|
|
1557
|
+
await settle();
|
|
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.
|
|
1561
|
+
model.value = ["de", "news", "tech"];
|
|
1562
|
+
await settle();
|
|
1563
|
+
expect(rowLabels().slice(0, 3)).toEqual(["Germany", "News", "Technology"]);
|
|
1564
|
+
|
|
1565
|
+
releasePointer({ x: 10, y: 130 });
|
|
1566
|
+
await settle();
|
|
1567
|
+
|
|
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"]);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1437
1599
|
it("clamps a row drag to the selected group", async () => {
|
|
1438
1600
|
const { events, container } = mountTagInput({ modelValue: ["news", "tech"] });
|
|
1439
1601
|
await openPanel(container);
|
|
@@ -2059,14 +2221,16 @@ describe("HkTagInput drag feedback and text selection", () => {
|
|
|
2059
2221
|
|
|
2060
2222
|
it("scales each list ALONG its drag axis and never across it", () => {
|
|
2061
2223
|
// The drop slot is resolved from the items' live rects
|
|
2062
|
-
// (usePointerReorder.indexAt)
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
2066
|
-
//
|
|
2067
|
-
//
|
|
2068
|
-
// the
|
|
2069
|
-
//
|
|
2224
|
+
// (usePointerReorder.indexAt) except for the item being dragged, whose
|
|
2225
|
+
// line across the list and midpoint along it come from the geometry
|
|
2226
|
+
// captured when the press landed: the item's midpoint on the drag axis
|
|
2227
|
+
// picks the slot, and each item's span on the axis ACROSS it decides
|
|
2228
|
+
// which line the pointer is on. A transform ON the pressed item is
|
|
2229
|
+
// measured from that pre-paint geometry either way, so the lift stays on
|
|
2230
|
+
// the drag axis by TASTE — a small scale following the gesture keeps a
|
|
2231
|
+
// dense chip row tight — rather than because the resolution needs it.
|
|
2232
|
+
// CSS is the only place the lift can regress, so each hook's own
|
|
2233
|
+
// transform is pinned.
|
|
2070
2234
|
const axisTransform = (hook: string): string | undefined =>
|
|
2071
2235
|
scss.match(
|
|
2072
2236
|
new RegExp(`${hook.replace(/[[\]]/g, "\\$&")}\\s*\\{\\s*transform:\\s*([^;]+);`),
|
|
@@ -155,11 +155,12 @@ type HkTagStop =
|
|
|
155
155
|
* lift scales ALONG each list's drag axis by TASTE, not by necessity: a
|
|
156
156
|
* small scale that follows the gesture keeps the lifted item's own
|
|
157
157
|
* footprint tight in a dense chip row. The drop slot is resolved from
|
|
158
|
-
* the items' live rects
|
|
159
|
-
* the
|
|
160
|
-
*
|
|
161
|
-
* transform on it — along the axis, across it, or
|
|
162
|
-
*
|
|
158
|
+
* the items' live rects except for the item being dragged: its line
|
|
159
|
+
* across the list and its midpoint along it come from the geometry
|
|
160
|
+
* captured when the press landed, before the lift is painted, so no
|
|
161
|
+
* transform on it — along the axis, across it, isotropic, or anchored
|
|
162
|
+
* on an edge rather than its centre — can move the slot the pointer
|
|
163
|
+
* resolves;
|
|
163
164
|
* - a key in `modelValue` that the catalog does not carry (a custom
|
|
164
165
|
* tag, or an option deleted since) degrades to its raw key as the
|
|
165
166
|
* tag label — the same rule as HkAffixPicker.tagEntries. Such a key
|
|
@@ -452,12 +453,23 @@ export const HkTagInput = defineComponent({
|
|
|
452
453
|
/** The panel's keyboard reorder: move the ACTIVE row one slot inside
|
|
453
454
|
* the selected group (the same edit a row drag makes, without a
|
|
454
455
|
* pointer). Only a selected row has a place in that order, the ends of
|
|
455
|
-
* the group are hard stops, and `false` means
|
|
456
|
-
*
|
|
457
|
-
* in the new order, so it follows the row the user moved. */
|
|
456
|
+
* the group are hard stops, and `false` means there was nothing this
|
|
457
|
+
* key could move. The cursor stays on the key it was on — the stops
|
|
458
|
+
* rebuild in the new order, so it follows the row the user moved. */
|
|
458
459
|
function nudgeActiveRow(delta: 1 | -1): boolean {
|
|
459
460
|
const stop = activeStop.value;
|
|
460
461
|
if (props.disabled || !stop || stop.kind !== "option") return false;
|
|
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
|
+
// 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).
|
|
472
|
+
if (chipDrag.pressed.value || rowDrag.pressed.value) return false;
|
|
461
473
|
const group = selectedRows.value;
|
|
462
474
|
const from = group.findIndex((option) => option.key === stop.option.key);
|
|
463
475
|
if (from < 0) return false;
|