@celestia-island/hikari 0.55.321 → 0.55.322
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, defineComponent, h, onBeforeUnmount, onMounted, ref, type Component, type PropType } from "vue";
|
|
1
|
+
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, ref, type Component, type PropType } from "vue";
|
|
2
2
|
import { MoreHorizontal } from "lucide-vue-next";
|
|
3
3
|
|
|
4
4
|
import HkCard from "./HkCard";
|
|
@@ -9,6 +9,7 @@ import HkTooltip from "./HkTooltip";
|
|
|
9
9
|
import { hasRenderedContent } from "../runtime/hasRenderedContent";
|
|
10
10
|
import { useMediaQuery } from "../runtime/useMediaQuery";
|
|
11
11
|
import type { StatTone } from "./HkStatCard";
|
|
12
|
+
import { HK_SCROLL_PROGRESS_REGISTRAR, type HkScrollProgressHandle } from "./HkScrollProgress";
|
|
12
13
|
import "./HkCardList.scss";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -43,7 +44,43 @@ const renderedRows = hasRenderedContent;
|
|
|
43
44
|
*/
|
|
44
45
|
export const HkCardList = defineComponent({
|
|
45
46
|
name: "HkCardList",
|
|
46
|
-
|
|
47
|
+
props: {
|
|
48
|
+
/** Authoritative row total the scroll-progress pill uses as its
|
|
49
|
+
* denominator (the server count behind a windowed/infinite list).
|
|
50
|
+
* Forwarded to the nearest enclosing HkScrollContainer with
|
|
51
|
+
* `scrollProgress` on, exactly like HkAdminTablePage's `total`.
|
|
52
|
+
* Omitted: the pill falls back to counting the delivered DOM rows
|
|
53
|
+
* (right for fully-delivered lists, drifting low for windowed
|
|
54
|
+
* ones — pass the server count there). */
|
|
55
|
+
total: { type: Number, default: undefined },
|
|
56
|
+
},
|
|
57
|
+
setup(props, { slots }) {
|
|
58
|
+
// Scroll-progress registration (the HkTable precedent): the NEAREST
|
|
59
|
+
// enclosing HkScrollContainer with `scrollProgress` on provides the
|
|
60
|
+
// registrar — provide/inject scoping resolves the admin shell's
|
|
61
|
+
// viewport for console pages and keeps a list registered against an
|
|
62
|
+
// inner container out of the shell. The handle hands the pill the
|
|
63
|
+
// list element as the row root, the `total` prop live, and the
|
|
64
|
+
// rendered HkListRow elements as the rows (a card list's rows ARE
|
|
65
|
+
// its data rows — no section-band caveat).
|
|
66
|
+
const listRef = ref<HTMLElement | null>(null);
|
|
67
|
+
const progressRegistrar = inject(HK_SCROLL_PROGRESS_REGISTRAR, null);
|
|
68
|
+
const progressHandle: HkScrollProgressHandle = {
|
|
69
|
+
getRoot: () => listRef.value,
|
|
70
|
+
getTotal: () => props.total,
|
|
71
|
+
getRows: () =>
|
|
72
|
+
listRef.value
|
|
73
|
+
? Array.from(listRef.value.children).filter((el) =>
|
|
74
|
+
el.classList.contains("hk-list-row"),
|
|
75
|
+
)
|
|
76
|
+
: [],
|
|
77
|
+
};
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
progressRegistrar?.register(progressHandle);
|
|
80
|
+
});
|
|
81
|
+
onBeforeUnmount(() => {
|
|
82
|
+
progressRegistrar?.unregister(progressHandle);
|
|
83
|
+
});
|
|
47
84
|
return () => {
|
|
48
85
|
const rows = slots.default?.();
|
|
49
86
|
return (
|
|
@@ -51,7 +88,7 @@ export const HkCardList = defineComponent({
|
|
|
51
88
|
{slots.empty && !renderedRows(rows) ? (
|
|
52
89
|
slots.empty()
|
|
53
90
|
) : (
|
|
54
|
-
<div class="hk-card-list" role="list">
|
|
91
|
+
<div class="hk-card-list" role="list" ref={listRef}>
|
|
55
92
|
{rows}
|
|
56
93
|
</div>
|
|
57
94
|
)}
|
|
@@ -12,7 +12,12 @@ import { createApp, defineComponent, h, inject, nextTick, onMounted, onUnmounted
|
|
|
12
12
|
import HkScrollContainer from "./HkScrollContainer";
|
|
13
13
|
import { HkAdminTablePage } from "./HkAdminTablePage";
|
|
14
14
|
import HTable from "./HkTable";
|
|
15
|
-
import {
|
|
15
|
+
import { HkCardList, HkListRow } from "./HkCardList";
|
|
16
|
+
import {
|
|
17
|
+
HK_SCROLL_PROGRESS_REGISTRAR,
|
|
18
|
+
type HkScrollProgressHandle,
|
|
19
|
+
type HkScrollProgressRegistrar,
|
|
20
|
+
} from "./HkScrollProgress";
|
|
16
21
|
|
|
17
22
|
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
18
23
|
const rects: Array<ReturnType<typeof vi.spyOn>> = [];
|
|
@@ -485,6 +490,149 @@ describe("HkAdminTablePage scroll-progress registration", () => {
|
|
|
485
490
|
});
|
|
486
491
|
|
|
487
492
|
|
|
493
|
+
describe("HkCardList scroll-progress registration", () => {
|
|
494
|
+
it("registers its list root, rows and total with the provided registrar", async () => {
|
|
495
|
+
const registered: Array<{ getRoot: () => HTMLElement | null; getTotal: () => number | undefined; getRows?: () => readonly Element[] | undefined }> = [];
|
|
496
|
+
const unregistered: unknown[] = [];
|
|
497
|
+
const registrar: HkScrollProgressRegistrar = {
|
|
498
|
+
register: (handle) => registered.push(handle),
|
|
499
|
+
unregister: (handle) => unregistered.push(handle),
|
|
500
|
+
};
|
|
501
|
+
const Provider = defineComponent({
|
|
502
|
+
setup() {
|
|
503
|
+
provide(HK_SCROLL_PROGRESS_REGISTRAR, registrar);
|
|
504
|
+
return () =>
|
|
505
|
+
h(HkCardList, { total: 42 }, {
|
|
506
|
+
default: () => [
|
|
507
|
+
h(HkListRow, { key: "a", title: "a" }),
|
|
508
|
+
h(HkListRow, { key: "b", title: "b" }),
|
|
509
|
+
// A non-row child (a bare div a host slipped in) must not
|
|
510
|
+
// count as a data row — getRows filters to .hk-list-row.
|
|
511
|
+
h("div", { key: "spacer", class: "h-2" }),
|
|
512
|
+
],
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
const container = mountTree(h(Provider));
|
|
517
|
+
expect(registered.length).toBe(1);
|
|
518
|
+
expect(registered[0].getTotal()).toBe(42);
|
|
519
|
+
// The list element is the row root — the element containing the rows.
|
|
520
|
+
const root = registered[0].getRoot()!;
|
|
521
|
+
expect(root).not.toBeNull();
|
|
522
|
+
expect(root.classList.contains("hk-card-list")).toBe(true);
|
|
523
|
+
expect(root.querySelectorAll(".hk-list-row")).toHaveLength(2);
|
|
524
|
+
// getRows hands the pill the HkListRow children only (the spacer
|
|
525
|
+
// div is excluded) — the section-band precision HkTable carries,
|
|
526
|
+
// mirrored for card lists.
|
|
527
|
+
const rows = registered[0].getRows!();
|
|
528
|
+
expect(rows).toBeDefined();
|
|
529
|
+
expect(rows!).toHaveLength(2);
|
|
530
|
+
expect(rows!.every((el) => el.classList.contains("hk-list-row"))).toBe(true);
|
|
531
|
+
|
|
532
|
+
// Unmount unregisters the SAME handle.
|
|
533
|
+
mounts[0].app.unmount();
|
|
534
|
+
expect(unregistered.length).toBe(1);
|
|
535
|
+
expect(unregistered[0]).toBe(registered[0]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("keeps the handle honest across the empty-slot lifecycle (rows→empty→rows)", async () => {
|
|
539
|
+
// The list element unmounts when the empty slot renders: the handle
|
|
540
|
+
// must read a NULL root (the pill's hidden precondition — never a
|
|
541
|
+
// stale detached element), and a re-render must recover a fresh
|
|
542
|
+
// connected list under the SAME handle.
|
|
543
|
+
const registered: Array<HkScrollProgressHandle> = [];
|
|
544
|
+
const registrar: HkScrollProgressRegistrar = {
|
|
545
|
+
register: (handle) => registered.push(handle),
|
|
546
|
+
unregister: () => {},
|
|
547
|
+
};
|
|
548
|
+
const hasRows = ref(true);
|
|
549
|
+
const Provider = defineComponent({
|
|
550
|
+
setup() {
|
|
551
|
+
provide(HK_SCROLL_PROGRESS_REGISTRAR, registrar);
|
|
552
|
+
return () =>
|
|
553
|
+
h(HkCardList, { total: 9 }, {
|
|
554
|
+
default: () => (hasRows.value ? [h(HkListRow, { key: "a", title: "a" })] : []),
|
|
555
|
+
empty: () => h("div", {}, "empty"),
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
mountTree(h(Provider));
|
|
560
|
+
const handle = registered[0]!;
|
|
561
|
+
expect(handle.getRoot()).not.toBeNull();
|
|
562
|
+
expect(handle.getRows!()).toHaveLength(1);
|
|
563
|
+
|
|
564
|
+
// Rows → empty: the ref clears with the element (no stale root).
|
|
565
|
+
hasRows.value = false;
|
|
566
|
+
await nextTick();
|
|
567
|
+
expect(handle.getRoot()).toBeNull();
|
|
568
|
+
expect(handle.getRows!()).toHaveLength(0);
|
|
569
|
+
expect(handle.getTotal()).toBe(9); // the prop stays live
|
|
570
|
+
|
|
571
|
+
// Empty → rows: the same handle recovers a fresh connected list.
|
|
572
|
+
hasRows.value = true;
|
|
573
|
+
await nextTick();
|
|
574
|
+
const root = handle.getRoot()!;
|
|
575
|
+
expect(root).not.toBeNull();
|
|
576
|
+
expect(root.isConnected).toBe(true);
|
|
577
|
+
expect(root.classList.contains("hk-card-list")).toBe(true);
|
|
578
|
+
expect(handle.getRows!()).toHaveLength(1);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("renders its rows into the shell viewport's pill denominator end-to-end", async () => {
|
|
582
|
+
// The real provide/inject path: a card list inside a
|
|
583
|
+
// scrollProgress-on container registers itself, and the pill's
|
|
584
|
+
// denominator is the list's declared total (not the delivered rows)
|
|
585
|
+
// — the same end-to-end contract the table page carries.
|
|
586
|
+
const list = h(HkCardList, {
|
|
587
|
+
total: 500,
|
|
588
|
+
}, {
|
|
589
|
+
default: () => Array.from({ length: 30 }, (_, i) =>
|
|
590
|
+
h(HkListRow, { key: i, title: `r${i}` })),
|
|
591
|
+
});
|
|
592
|
+
const instance = ref<{ refresh: () => void } | null>(null);
|
|
593
|
+
let scrollTop = 0;
|
|
594
|
+
const Wrapper = defineComponent({
|
|
595
|
+
setup() {
|
|
596
|
+
return () =>
|
|
597
|
+
h(HkScrollContainer, { ref: instance as never, scrollProgress: true }, { default: () => list });
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
const container = mountTree(h(Wrapper));
|
|
601
|
+
const viewport = container.querySelector<HTMLElement>(".hk-scroll-container-viewport")!;
|
|
602
|
+
Object.defineProperties(viewport, {
|
|
603
|
+
scrollHeight: { configurable: true, get: () => 4000 },
|
|
604
|
+
clientHeight: { configurable: true, get: () => 400 },
|
|
605
|
+
scrollWidth: { configurable: true, get: () => 300 },
|
|
606
|
+
clientWidth: { configurable: true, get: () => 300 },
|
|
607
|
+
scrollLeft: { configurable: true, get: () => 0 },
|
|
608
|
+
scrollTop: { configurable: true, get: () => scrollTop },
|
|
609
|
+
});
|
|
610
|
+
rects.push(vi.spyOn(viewport, "getBoundingClientRect").mockReturnValue({
|
|
611
|
+
left: 0, top: 0, right: 300, bottom: 400, width: 300, height: 400, x: 0, y: 0,
|
|
612
|
+
} as DOMRect));
|
|
613
|
+
const listRoot = container.querySelector<HTMLElement>(".hk-card-list")!;
|
|
614
|
+
expect(listRoot).not.toBeNull();
|
|
615
|
+
rects.push(vi.spyOn(listRoot, "getBoundingClientRect").mockReturnValue({
|
|
616
|
+
left: 0, top: -200, right: 300, bottom: 3800, width: 300, height: 4000, x: 0, y: -200,
|
|
617
|
+
} as DOMRect));
|
|
618
|
+
container.querySelectorAll<HTMLElement>(".hk-list-row").forEach((row, i) => {
|
|
619
|
+
const top = i * 40 - 200;
|
|
620
|
+
rects.push(vi.spyOn(row, "getBoundingClientRect").mockReturnValue({
|
|
621
|
+
left: 0, top, right: 300, bottom: top + 40, width: 300, height: 40, x: 0, y: top,
|
|
622
|
+
} as DOMRect));
|
|
623
|
+
});
|
|
624
|
+
scrollTop = 200;
|
|
625
|
+
instance.value!.refresh();
|
|
626
|
+
await nextTick();
|
|
627
|
+
const pill = container.querySelector<HTMLElement>(".hk-scroll-progress-pill");
|
|
628
|
+
expect(pill).not.toBeNull();
|
|
629
|
+
// First visible row #6 over the DECLARED total — the delivered 30
|
|
630
|
+
// rows never override the list's server count.
|
|
631
|
+
expect(pill!.textContent).toBe("6 / 500");
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
|
|
488
636
|
// ── SCSS contract (textual pins — happy-dom does not lay out) ────────
|
|
489
637
|
// The house style pins sheet claims with readFileSync probes
|
|
490
638
|
// (HkButton.test.tsx icon-only precedent): these guard the paint
|