@celestia-island/hikari 0.55.146 → 0.55.147
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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkBoard, { type BoardNodeInput } from "./HkBoard";
|
|
5
|
+
|
|
6
|
+
/** Pointer event the suite can dispatch (happy-dom has PointerEvent). */
|
|
7
|
+
function pointerEvent(type: string, x: number, y: number): Event {
|
|
8
|
+
const Ctor = (globalThis as { PointerEvent?: typeof MouseEvent }).PointerEvent ?? MouseEvent;
|
|
9
|
+
const ev = new Ctor(type, { bubbles: true, cancelable: true, clientX: x, clientY: y });
|
|
10
|
+
(ev as { pointerId?: number }).pointerId = 1;
|
|
11
|
+
return ev;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const nodes: BoardNodeInput[] = [
|
|
15
|
+
{ id: "a", x: 0, y: 0, w: 100, h: 60, draggable: true, label: "A" },
|
|
16
|
+
{ id: "b", x: 160, y: 0, w: 100, h: 60, draggable: true, label: "B" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** Mounts the board with the given slots, the repo's own way (no
|
|
20
|
+
* @vue/test-utils in this package): a throwaway app on document.body. */
|
|
21
|
+
function mountBoard(slots: Record<string, () => unknown> = {}, onNodeMove?: () => void): unknown {
|
|
22
|
+
const host = document.createElement("div");
|
|
23
|
+
document.body.appendChild(host);
|
|
24
|
+
// (defineComponent(fn) treats fn as a FUNCTIONAL component — the wrapper
|
|
25
|
+
// needs the explicit setup form to hand HkBoard its slots.)
|
|
26
|
+
const wrapper = defineComponent({
|
|
27
|
+
setup: () => () => h(HkBoard, { nodes, onNodeMove }, slots),
|
|
28
|
+
});
|
|
29
|
+
const app = createApp(wrapper);
|
|
30
|
+
app.mount(host);
|
|
31
|
+
return app;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
document.body.innerHTML = "";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("HkBoard nodeActions slot", () => {
|
|
39
|
+
it("renders the slot per node, docked inside the node shell", () => {
|
|
40
|
+
mountBoard({
|
|
41
|
+
nodeActions: () => [
|
|
42
|
+
h("button", { "data-test": "act" }, "act"),
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
const docks = [...document.body.querySelectorAll<HTMLElement>(".hk-board-node-actions")];
|
|
46
|
+
expect(docks).toHaveLength(2);
|
|
47
|
+
expect(docks[0].querySelector("[data-test=act]")).toBeTruthy();
|
|
48
|
+
expect(docks[1].querySelector("[data-test=act]")).toBeTruthy();
|
|
49
|
+
// The dock lives INSIDE the node shell (the SCSS anchors it to the
|
|
50
|
+
// node's bottom-end corner — pinned by the source contract below).
|
|
51
|
+
expect(docks[0].closest(".hk-board-node")).toBeTruthy();
|
|
52
|
+
expect(docks[0].parentElement!.classList.contains("hk-board-node")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders nothing when the slot is absent", () => {
|
|
56
|
+
mountBoard();
|
|
57
|
+
expect(document.body.querySelector(".hk-board-node-actions")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps a press that starts on a dock action from becoming a node drag", () => {
|
|
61
|
+
// The board captures the pointer on its own root for draggable nodes;
|
|
62
|
+
// a dock press must not enter that gesture (upstream HkMinimap guards
|
|
63
|
+
// its zoom chrome the same way). happy-dom cannot emulate capture, so
|
|
64
|
+
// the observable contract here is the DRAG: a pointer that travels
|
|
65
|
+
// after starting on the dock leaves the node where it was.
|
|
66
|
+
const onNodeMove = vi.fn();
|
|
67
|
+
const host = document.createElement("div");
|
|
68
|
+
document.body.appendChild(host);
|
|
69
|
+
const app = mountBoard(
|
|
70
|
+
{ nodeActions: () => [h("button", { "data-test": "act" }, "act")] },
|
|
71
|
+
onNodeMove,
|
|
72
|
+
) as { unmount: () => void };
|
|
73
|
+
|
|
74
|
+
const dock = document.body.querySelector<HTMLElement>(".hk-board-node-actions")!;
|
|
75
|
+
const shell = dock.closest(".hk-board-node") as HTMLElement;
|
|
76
|
+
const before = shell.style.left;
|
|
77
|
+
dock.dispatchEvent(pointerEvent("pointerdown", 90, 50));
|
|
78
|
+
shell.dispatchEvent(pointerEvent("pointermove", 400, 50));
|
|
79
|
+
shell.dispatchEvent(pointerEvent("pointerup", 400, 50));
|
|
80
|
+
expect(shell.style.left).toBe(before);
|
|
81
|
+
expect(onNodeMove).not.toHaveBeenCalled();
|
|
82
|
+
app.unmount();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** Source contract for the dock's load-bearing rules (vitest cannot see
|
|
87
|
+
* stylesheets — a deleted rule leaves every DOM guard green). */
|
|
88
|
+
const boardScss = (await import("node:fs")).readFileSync(
|
|
89
|
+
(await import("node:path")).resolve(__dirname, "./HkBoard.scss"),
|
|
90
|
+
"utf8",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
describe("HkBoard nodeActions dock styles", () => {
|
|
94
|
+
it("docks the actions to the node's bottom-end corner above the body", () => {
|
|
95
|
+
const block = boardScss.slice(boardScss.indexOf("&-actions {"));
|
|
96
|
+
expect(block.slice(0, 400)).toMatch(/position:\s*absolute/);
|
|
97
|
+
expect(block.slice(0, 400)).toMatch(/inset-inline-end/);
|
|
98
|
+
expect(block.slice(0, 400)).toMatch(/bottom:/);
|
|
99
|
+
expect(block.slice(0, 400)).toMatch(/z-index/);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -44,6 +44,21 @@
|
|
|
44
44
|
position: absolute;
|
|
45
45
|
box-sizing: border-box;
|
|
46
46
|
|
|
47
|
+
// Per-node action dock (the `nodeActions` slot): consumer-injected
|
|
48
|
+
// controls ride the node's bottom-end corner above the body, so hosts
|
|
49
|
+
// stop hand-drawing their own buttons into the node slot. The dock owns
|
|
50
|
+
// its own capture-safety (see HkBoard.tsx) — a press that starts here
|
|
51
|
+
// never becomes a node drag.
|
|
52
|
+
&-actions {
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset-inline-end: var(--hk-board-node-action-inset, 4px);
|
|
55
|
+
bottom: var(--hk-board-node-action-inset, 4px);
|
|
56
|
+
z-index: 6;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 4px;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
&--draggable {
|
|
48
63
|
cursor: grab;
|
|
49
64
|
|
|
@@ -453,6 +453,26 @@ export default defineComponent({
|
|
|
453
453
|
{slots.node
|
|
454
454
|
? slots.node({ node: n })
|
|
455
455
|
: <div class="hk-board-node-shell">{n.label ?? ""}</div>}
|
|
456
|
+
{slots.nodeActions
|
|
457
|
+
? (
|
|
458
|
+
<div
|
|
459
|
+
class="hk-board-node-actions"
|
|
460
|
+
// A press that starts on an action belongs to that
|
|
461
|
+
// action: this component captures the pointer on its own
|
|
462
|
+
// root for every draggable node, and a captured press
|
|
463
|
+
// retargets the following events to the ancestor — an
|
|
464
|
+
// action button's own click would never fire. Stopping
|
|
465
|
+
// the bubble here is the same guard HkMinimap puts on
|
|
466
|
+
// its zoom chrome. The keydown twin keeps a focused
|
|
467
|
+
// action's Enter/Space from reaching the node's own key
|
|
468
|
+
// handling.
|
|
469
|
+
onPointerdown={(e: PointerEvent) => e.stopPropagation()}
|
|
470
|
+
onKeydown={(e: KeyboardEvent) => e.stopPropagation()}
|
|
471
|
+
>
|
|
472
|
+
{slots.nodeActions({ node: n })}
|
|
473
|
+
</div>
|
|
474
|
+
)
|
|
475
|
+
: null}
|
|
456
476
|
</div>
|
|
457
477
|
))}
|
|
458
478
|
</div>
|