@celestia-island/hikari 0.43.7 → 0.43.8
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,130 @@ 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("ends a row drag 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:
|
|
1559
|
+
// the index the drag resolves by no longer names the row it grabbed.
|
|
1560
|
+
model.value = ["de", "news", "tech"];
|
|
1561
|
+
await settle();
|
|
1562
|
+
expect(rowLabels().slice(0, 3)).toEqual(["Germany", "News", "Technology"]);
|
|
1563
|
+
|
|
1564
|
+
releasePointer({ x: 10, y: 130 });
|
|
1565
|
+
await settle();
|
|
1566
|
+
|
|
1567
|
+
// The gesture ends without a reorder — a drag may come to nothing, it
|
|
1568
|
+
// must never move whichever row slid into the index it was holding.
|
|
1569
|
+
expect(events.modelValue, "no reorder came out of the rewritten strip").toEqual([]);
|
|
1570
|
+
expect(tagTexts(container)).toEqual(["Germany", "News", "Technology"]);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1437
1573
|
it("clamps a row drag to the selected group", async () => {
|
|
1438
1574
|
const { events, container } = mountTagInput({ modelValue: ["news", "tech"] });
|
|
1439
1575
|
await openPanel(container);
|
|
@@ -2059,14 +2195,16 @@ describe("HkTagInput drag feedback and text selection", () => {
|
|
|
2059
2195
|
|
|
2060
2196
|
it("scales each list ALONG its drag axis and never across it", () => {
|
|
2061
2197
|
// The drop slot is resolved from the items' live rects
|
|
2062
|
-
// (usePointerReorder.indexAt)
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
2066
|
-
//
|
|
2067
|
-
//
|
|
2068
|
-
// the
|
|
2069
|
-
//
|
|
2198
|
+
// (usePointerReorder.indexAt) except for the item being dragged, whose
|
|
2199
|
+
// line across the list and midpoint along it come from the geometry
|
|
2200
|
+
// captured when the press landed: the item's midpoint on the drag axis
|
|
2201
|
+
// picks the slot, and each item's span on the axis ACROSS it decides
|
|
2202
|
+
// which line the pointer is on. A transform ON the pressed item is
|
|
2203
|
+
// measured from that pre-paint geometry either way, so the lift stays on
|
|
2204
|
+
// the drag axis by TASTE — a small scale following the gesture keeps a
|
|
2205
|
+
// dense chip row tight — rather than because the resolution needs it.
|
|
2206
|
+
// CSS is the only place the lift can regress, so each hook's own
|
|
2207
|
+
// transform is pinned.
|
|
2070
2208
|
const axisTransform = (hook: string): string | undefined =>
|
|
2071
2209
|
scss.match(
|
|
2072
2210
|
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,22 @@ 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 resolves its landing slot by INDEX, so a reorder under the
|
|
465
|
+
// held pointer (or under a press that is about to become one) would
|
|
466
|
+
// leave the release moving whatever now sits at that index — a
|
|
467
|
+
// different row, silently. The chord is still consumed by the caller
|
|
468
|
+
// either way (Alt+Arrow is not a plain arrow, and the browser must
|
|
469
|
+
// not act on it); it just has nothing to move while a pointer owns
|
|
470
|
+
// the list.
|
|
471
|
+
if (chipDrag.pressed.value || rowDrag.pressed.value) return false;
|
|
461
472
|
const group = selectedRows.value;
|
|
462
473
|
const from = group.findIndex((option) => option.key === stop.option.key);
|
|
463
474
|
if (from < 0) return false;
|