@celestia-island/hikari 0.45.2 → 0.45.3
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,123 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkModal from "./HkModal";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HkModal footerLead slot contract tests:
|
|
8
|
+
* - the named slot renders inside .hk-modal-footer BEFORE the action buttons,
|
|
9
|
+
* wrapped in .hk-modal-footer-lead, with the band marked .hk-modal-footer--lead
|
|
10
|
+
* - the band still renders with ONLY a footerLead (no footerActions) — a lead
|
|
11
|
+
* alone must not vanish with the actions
|
|
12
|
+
* - absent the slot, no new DOM appears (backward compat)
|
|
13
|
+
* - the explicit `footer` slot keeps precedence over footerLead + footerActions
|
|
14
|
+
* (existing full-custom contract unchanged)
|
|
15
|
+
*/
|
|
16
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
20
|
+
app.unmount();
|
|
21
|
+
container.remove();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
interface Harness {
|
|
26
|
+
container: HTMLElement;
|
|
27
|
+
footer: HTMLElement | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function mountModal(
|
|
31
|
+
props: Record<string, unknown>,
|
|
32
|
+
slots: Record<string, unknown> = {},
|
|
33
|
+
): Promise<Harness> {
|
|
34
|
+
const container = document.createElement("div");
|
|
35
|
+
document.body.appendChild(container);
|
|
36
|
+
const open = ref(true);
|
|
37
|
+
const Wrapper = defineComponent({
|
|
38
|
+
setup() {
|
|
39
|
+
// A FRESH slots object per Wrapper render: updateSlots mutates the
|
|
40
|
+
// slots object it is handed (deleting keys absent from a later
|
|
41
|
+
// sibling mount's shape), and a shared literal would lose footerLead
|
|
42
|
+
// on re-render.
|
|
43
|
+
return () =>
|
|
44
|
+
h(HkModal, {
|
|
45
|
+
...props,
|
|
46
|
+
modelValue: open.value,
|
|
47
|
+
"onUpdate:modelValue": (v: boolean) => { open.value = v; },
|
|
48
|
+
}, { ...slots });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const app = createApp(Wrapper);
|
|
52
|
+
app.mount(container);
|
|
53
|
+
mounts.push({ app, container });
|
|
54
|
+
await nextTick();
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
56
|
+
await nextTick();
|
|
57
|
+
// HkModal teleports to body and sibling test files in this worker can
|
|
58
|
+
// leave their own modals behind — scope to the NEWEST mounted footer
|
|
59
|
+
// instead of the document-first match.
|
|
60
|
+
const footers = document.querySelectorAll(".hk-modal-footer");
|
|
61
|
+
return {
|
|
62
|
+
container,
|
|
63
|
+
footer: footers.length ? (footers[footers.length - 1] as HTMLElement) : null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("HkModal footerLead slot", () => {
|
|
68
|
+
it("renders the lead before the action buttons inside the band", async () => {
|
|
69
|
+
const { footer } = await mountModal(
|
|
70
|
+
{
|
|
71
|
+
title: "Config",
|
|
72
|
+
footerActions: [{ label: "Add", variant: "primary" }],
|
|
73
|
+
},
|
|
74
|
+
{ footerLead: () => h("button", { class: "lead-jump", "data-test": "jump" }, "JUMP") },
|
|
75
|
+
);
|
|
76
|
+
expect(footer).not.toBeNull();
|
|
77
|
+
expect(footer?.classList.contains("hk-modal-footer--lead")).toBe(true);
|
|
78
|
+
const lead = footer?.querySelector(".hk-modal-footer-lead");
|
|
79
|
+
expect(lead).not.toBeNull();
|
|
80
|
+
expect(lead?.querySelector(".lead-jump")?.textContent).toBe("JUMP");
|
|
81
|
+
|
|
82
|
+
const children = footer ? Array.from(footer.children) : [];
|
|
83
|
+
expect(children[0]?.classList.contains("hk-modal-footer-lead")).toBe(true);
|
|
84
|
+
expect(children.length).toBe(2);
|
|
85
|
+
expect(children[1]?.textContent).toBe("Add");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("keeps the band alive with ONLY a footerLead and no actions", async () => {
|
|
89
|
+
const { footer } = await mountModal(
|
|
90
|
+
{ title: "Config" },
|
|
91
|
+
{ footerLead: () => h("button", { class: "lead-jump" }, "JUMP") },
|
|
92
|
+
);
|
|
93
|
+
expect(footer).not.toBeNull();
|
|
94
|
+
expect(footer?.classList.contains("hk-modal-footer--lead")).toBe(true);
|
|
95
|
+
expect(footer?.querySelector(".hk-modal-footer-lead")).not.toBeNull();
|
|
96
|
+
// No action buttons — the only children are the lead wrapper.
|
|
97
|
+
const children = footer ? Array.from(footer.children) : [];
|
|
98
|
+
expect(children).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("adds no lead DOM and no band when both lead and actions are absent", async () => {
|
|
102
|
+
const { footer } = await mountModal({ title: "Config" });
|
|
103
|
+
expect(footer).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("the explicit footer slot keeps precedence over footerLead and actions", async () => {
|
|
107
|
+
const { footer } = await mountModal(
|
|
108
|
+
{
|
|
109
|
+
title: "Config",
|
|
110
|
+
footerActions: [{ label: "Add" }],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
footer: () => h("div", { class: "custom-footer" }, "CUSTOM"),
|
|
114
|
+
footerLead: () => h("button", { class: "lead-jump" }, "JUMP"),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
expect(footer).not.toBeNull();
|
|
118
|
+
expect(footer?.classList.contains("hk-modal-footer--lead")).toBe(false);
|
|
119
|
+
expect(footer?.querySelector(".custom-footer")?.textContent).toBe("CUSTOM");
|
|
120
|
+
expect(footer?.querySelector(".hk-modal-footer-lead")).toBeNull();
|
|
121
|
+
expect(footer?.textContent).not.toContain("Add");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -298,6 +298,21 @@
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
// Lead content (e.g. a ghost jump link into the admin console) rendered at
|
|
302
|
+
// the footer's inline-start with the action buttons still pinned at
|
|
303
|
+
// inline-end — the footer counterpart of .hk-modal-header-lead. The lead's
|
|
304
|
+
// auto inline-end margin absorbs the slack (same distribution trick the
|
|
305
|
+
// header uses), so the layout mirrors correctly in RTL without a
|
|
306
|
+
// space-between override.
|
|
307
|
+
.hk-modal-footer--lead {
|
|
308
|
+
.hk-modal-footer-lead {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
flex-shrink: 0;
|
|
312
|
+
margin-inline-end: auto;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
301
316
|
// ------
|
|
302
317
|
// Responsive — full-screen on narrow viewports
|
|
303
318
|
// ------
|
|
@@ -695,10 +695,13 @@ export default defineComponent({
|
|
|
695
695
|
if (slots.footer) {
|
|
696
696
|
return <div class="hk-modal-footer">{slots.footer()}</div>;
|
|
697
697
|
}
|
|
698
|
-
|
|
698
|
+
const hasLead = Boolean(slots.footerLead);
|
|
699
|
+
const hasActions = Boolean(props.footerActions && props.footerActions.length > 0);
|
|
700
|
+
if (hasLead || hasActions) {
|
|
699
701
|
return (
|
|
700
|
-
<div class="hk-modal-footer">
|
|
701
|
-
{
|
|
702
|
+
<div class={["hk-modal-footer", hasLead ? "hk-modal-footer--lead" : ""]}>
|
|
703
|
+
{hasLead && <div class="hk-modal-footer-lead">{slots.footerLead?.()}</div>}
|
|
704
|
+
{props.footerActions?.map((action, i) => (
|
|
702
705
|
<HButton
|
|
703
706
|
key={i}
|
|
704
707
|
variant={action.variant ?? "secondary"}
|