@celestia-island/hikari 0.55.184 → 0.55.185
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
|
@@ -108,6 +108,23 @@
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/* The per-row actions lane: renders only when the host supplies the
|
|
112
|
+
* `actions` slot (render-side guard), as a SIBLING of the content lane
|
|
113
|
+
* inside the row's flex layout — never a child of it, so the content
|
|
114
|
+
* lane's `> :only-child` fill above keeps stretching single-root rows
|
|
115
|
+
* even when actions ride along. Pinning is structural: the content lane
|
|
116
|
+
* keeps flex: 1 and this lane only refuses to shrink, so the actions
|
|
117
|
+
* land on the row's far edge — no margin-left:auto patch (it would
|
|
118
|
+
* paper over a content-lane regression instead of letting the fill rule
|
|
119
|
+
* fail loudly) and no directional property, so the lane follows the
|
|
120
|
+
* row's inline direction under RTL too. */
|
|
121
|
+
.hk-draggable-list-actions {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: var(--space-8, 0.5rem);
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
.hk-draggable-list-ghost {
|
|
112
129
|
position: fixed;
|
|
113
130
|
top: 0;
|
|
@@ -81,3 +81,30 @@ describe("HkDraggableList content-lane fill sheet", () => {
|
|
|
81
81
|
expect(fillRules[0]).toContain(":only-child");
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
describe("HkDraggableList actions-lane sheet", () => {
|
|
86
|
+
const css = listCss();
|
|
87
|
+
|
|
88
|
+
it("keeps the actions lane fixed at its natural width", () => {
|
|
89
|
+
const body = ruleBody(css, ".hk-draggable-list-actions");
|
|
90
|
+
expect(body, "the actions-lane rule must exist").not.toBeNull();
|
|
91
|
+
expect(body).toContain("display: flex");
|
|
92
|
+
expect(body).toContain("align-items: center");
|
|
93
|
+
expect(body).toContain("flex-shrink: 0");
|
|
94
|
+
// Multiple host buttons in one lane need the lane's own gap so they
|
|
95
|
+
// never fuse into one cluster against the row edge.
|
|
96
|
+
expect(body).toContain("gap:");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// The right-edge pinning is STRUCTURAL, not a margin patch: the item
|
|
100
|
+
// row is a flex row and the content lane keeps flex: 1, so the lane
|
|
101
|
+
// needs nothing beyond shrink-resistance. A margin-left: auto here
|
|
102
|
+
// would paper over a future content-lane regression instead of
|
|
103
|
+
// letting the fill sheet fail loudly — pin by ownership, not by patch.
|
|
104
|
+
it("pins by the content lane's flex: 1, not a margin patch", () => {
|
|
105
|
+
const body = ruleBody(css, ".hk-draggable-list-actions");
|
|
106
|
+
expect(body).not.toContain("margin-left: auto");
|
|
107
|
+
const content = ruleBody(css, ".hk-draggable-list-content");
|
|
108
|
+
expect(content).toContain("flex: 1");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -390,3 +390,138 @@ describe("HkDraggableList press ergonomics (clickable rows)", () => {
|
|
|
390
390
|
}
|
|
391
391
|
});
|
|
392
392
|
});
|
|
393
|
+
|
|
394
|
+
describe("HkDraggableList actions lane", () => {
|
|
395
|
+
/** The standard row-actions contract: hosts inject trailing buttons
|
|
396
|
+
* through the scoped `actions` slot instead of hand-rolling
|
|
397
|
+
* ml-auto/flex-1 spacers inside the row content (chest's pricing
|
|
398
|
+
* ladder shipped exactly such a hand-rolled group and its buttons
|
|
399
|
+
* hugged the row's text mid-card). The lane renders as a SIBLING of
|
|
400
|
+
* the content lane — never inside it — so the content sheet's
|
|
401
|
+
* `> :only-child` fill keeps matching single-root rows even when
|
|
402
|
+
* actions ride along (sheet twin: HkDraggableList.styles.test.ts). */
|
|
403
|
+
|
|
404
|
+
function mountWithActions(items: { key: string }[], opts?: { lockedKeys?: string[] }): HTMLElement {
|
|
405
|
+
const container = document.createElement("div");
|
|
406
|
+
document.body.appendChild(container);
|
|
407
|
+
const app = createApp({
|
|
408
|
+
render: () =>
|
|
409
|
+
h(
|
|
410
|
+
HDraggableList,
|
|
411
|
+
{ items, lockedKeys: opts?.lockedKeys ?? [] },
|
|
412
|
+
{
|
|
413
|
+
item: ({ item }: { item: { key: string } }) =>
|
|
414
|
+
h("span", { class: "row-body" }, item.key),
|
|
415
|
+
actions: ({
|
|
416
|
+
item,
|
|
417
|
+
index,
|
|
418
|
+
locked,
|
|
419
|
+
}: {
|
|
420
|
+
item: { key: string };
|
|
421
|
+
index: number;
|
|
422
|
+
locked: boolean;
|
|
423
|
+
}) =>
|
|
424
|
+
h("span", {
|
|
425
|
+
class: "row-actions",
|
|
426
|
+
"data-key": item.key,
|
|
427
|
+
"data-index": index,
|
|
428
|
+
"data-locked": locked,
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
});
|
|
433
|
+
app.mount(container);
|
|
434
|
+
mounts.push({ app, container });
|
|
435
|
+
return container.querySelector(".hk-draggable-list") as HTMLElement;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
it("renders a right-lane sibling per row only when the slot is provided", () => {
|
|
439
|
+
const root = mountWithActions([{ key: "a" }, { key: "b" }]);
|
|
440
|
+
|
|
441
|
+
const lanes = Array.from(root.querySelectorAll(".hk-draggable-list-actions"));
|
|
442
|
+
expect(lanes).toHaveLength(2);
|
|
443
|
+
for (const lane of lanes) {
|
|
444
|
+
// Sibling of the content lane, not a child of it — the :only-child
|
|
445
|
+
// fill contract above must keep matching the row body.
|
|
446
|
+
expect(lane.closest(".hk-draggable-list-content")).toBeNull();
|
|
447
|
+
expect(lane.previousElementSibling?.classList.contains("hk-draggable-list-content")).toBe(
|
|
448
|
+
true,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("keeps the content lane's :only-child fill intact alongside the lane", () => {
|
|
454
|
+
const root = mountWithActions([{ key: "a" }]);
|
|
455
|
+
|
|
456
|
+
const content = root.querySelector(".hk-draggable-list-content") as HTMLElement;
|
|
457
|
+
const elementChildren = Array.from(content.children);
|
|
458
|
+
expect(elementChildren).toHaveLength(1);
|
|
459
|
+
expect(elementChildren[0]!.classList.contains("row-body")).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("scopes the slot with item, index, and locked-ness", () => {
|
|
463
|
+
const root = mountWithActions([{ key: "a" }, { key: "b" }], { lockedKeys: ["b"] });
|
|
464
|
+
|
|
465
|
+
const marks = Array.from(root.querySelectorAll(".row-actions")) as HTMLElement[];
|
|
466
|
+
expect(marks).toHaveLength(2);
|
|
467
|
+
expect(marks.map((m) => `${m.dataset.key}:${m.dataset.index}:${m.dataset.locked}`).join(",")).toBe(
|
|
468
|
+
"a:0:false,b:1:true",
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("renders no lane for hosts that do not adopt the slot", () => {
|
|
473
|
+
const root = mountList(["a"], []);
|
|
474
|
+
|
|
475
|
+
expect(root.querySelectorAll(".hk-draggable-list-actions")).toHaveLength(0);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("keeps a press on a lane button from arming a drag — the handle still drags", async () => {
|
|
479
|
+
/** The lane exists to host buttons, so the row's interactive-content
|
|
480
|
+
* exclusion (button, a, input, textarea, select, [contenteditable])
|
|
481
|
+
* must own every press that starts on one — and a lane FULL of
|
|
482
|
+
* buttons must leave the handle's drag path untouched. */
|
|
483
|
+
const PointerEventCtor: typeof MouseEvent =
|
|
484
|
+
(globalThis as { PointerEvent?: typeof MouseEvent }).PointerEvent ?? MouseEvent;
|
|
485
|
+
const fire = (el: Element, type: string, x = 0, y = 0) =>
|
|
486
|
+
el.dispatchEvent(
|
|
487
|
+
new PointerEventCtor(type, { bubbles: true, cancelable: true, button: 0, clientX: x, clientY: y }),
|
|
488
|
+
);
|
|
489
|
+
const fireWin = (type: string, x = 0, y = 0) =>
|
|
490
|
+
window.dispatchEvent(new PointerEventCtor(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
491
|
+
|
|
492
|
+
const container = document.createElement("div");
|
|
493
|
+
document.body.appendChild(container);
|
|
494
|
+
const app = createApp({
|
|
495
|
+
render: () =>
|
|
496
|
+
h(HDraggableList, { items: [{ key: "a" }] }, {
|
|
497
|
+
item: ({ item }: { item: { key: string } }) => h("span", { class: "row-body" }, item.key),
|
|
498
|
+
actions: () => [
|
|
499
|
+
h("button", { class: "lane-button", type: "button" }, "✏"),
|
|
500
|
+
h("button", { class: "lane-button", type: "button" }, "⋯"),
|
|
501
|
+
],
|
|
502
|
+
}),
|
|
503
|
+
});
|
|
504
|
+
app.mount(container);
|
|
505
|
+
mounts.push({ app, container });
|
|
506
|
+
const root = container.querySelector(".hk-draggable-list") as HTMLElement;
|
|
507
|
+
const row = root.querySelector(".hk-draggable-list-item") as HTMLElement;
|
|
508
|
+
const button = row.querySelector(".lane-button") as HTMLElement;
|
|
509
|
+
const handle = row.querySelector(".hk-draggable-list-handle") as HTMLElement;
|
|
510
|
+
|
|
511
|
+
// Press ON the button (target = button, bubbling to the row) and
|
|
512
|
+
// travel far past the arm threshold: no drag may publish.
|
|
513
|
+
fire(button, "pointerdown", 10, 10);
|
|
514
|
+
fireWin("pointermove", 30, 40);
|
|
515
|
+
await nextTick();
|
|
516
|
+
expect(row.hasAttribute("data-dragging")).toBe(false);
|
|
517
|
+
|
|
518
|
+
// The same gesture on the handle still arms — a button-filled lane
|
|
519
|
+
// never breaks the drag path.
|
|
520
|
+
fire(handle, "pointerdown", 10, 10);
|
|
521
|
+
fireWin("pointermove", 30, 40);
|
|
522
|
+
await nextTick();
|
|
523
|
+
expect(row.getAttribute("data-dragging")).not.toBeNull();
|
|
524
|
+
|
|
525
|
+
fireWin("pointerup");
|
|
526
|
+
});
|
|
527
|
+
});
|
|
@@ -334,6 +334,19 @@ export default defineComponent({
|
|
|
334
334
|
<div class="hk-draggable-list-content">
|
|
335
335
|
{slots.item?.({ item, index, locked })}
|
|
336
336
|
</div>
|
|
337
|
+
{/* The standard row-actions lane: when the host provides
|
|
338
|
+
* an `actions` slot, every row renders this right-pinned
|
|
339
|
+
* lane (the content lane keeps flex: 1, so the buttons
|
|
340
|
+
* land on the row's far edge structurally — hosts stop
|
|
341
|
+
* hand-rolling ml-auto/flex-1 spacers). The lane is
|
|
342
|
+
* host-decided content: locked rows may still carry
|
|
343
|
+
* actions (chest's pricing ladder), so locked-ness is
|
|
344
|
+
* only reported through the slot scope. */}
|
|
345
|
+
{slots.actions ? (
|
|
346
|
+
<div class="hk-draggable-list-actions">
|
|
347
|
+
{slots.actions({ item, index, locked })}
|
|
348
|
+
</div>
|
|
349
|
+
) : null}
|
|
337
350
|
</div>
|
|
338
351
|
);
|
|
339
352
|
})}
|