@cosmicdrift/kumiko-renderer-web 0.171.0 → 0.171.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.171.0",
3
+ "version": "0.171.2",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.171.0",
20
- "@cosmicdrift/kumiko-headless": "0.171.0",
21
- "@cosmicdrift/kumiko-renderer": "0.171.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.171.2",
20
+ "@cosmicdrift/kumiko-headless": "0.171.2",
21
+ "@cosmicdrift/kumiko-renderer": "0.171.2",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -472,12 +472,17 @@ describe("DataTable", () => {
472
472
 
473
473
  // Infinite-Scroll Sentinel: rendert sentinel-div, zeigt Spinner wenn
474
474
  // loadingMore, den i18n End-of-list-Marker (kumiko.list.end-of-list)
475
- // wenn !hasMore. IntersectionObserver
475
+ // wenn !hasMore und genug Zeilen geladen sind (kurze Listen brauchen
476
+ // keinen Marker — sie zeigen ihr Ende selbst). IntersectionObserver
476
477
  // selbst ist in jsdom unmocked — wir testen nur die Marker, der
477
478
  // Observer-Fire-Pfad ist im KumikoScreen.EntityListBody.
478
479
  describe("InfiniteSentinel", () => {
479
480
  const cols = [{ field: "name", label: "Name", type: "string", sortable: false }] as const;
480
481
  const oneRow = [{ id: "r1", values: { name: "A" } }];
482
+ const manyRows = Array.from({ length: 25 }, (_, i) => ({
483
+ id: `r${i}`,
484
+ values: { name: `A${i}` },
485
+ }));
481
486
 
482
487
  test("ohne onReachEnd: kein Sentinel im DOM", () => {
483
488
  render(<DataTable columns={cols} rows={oneRow} testId="dt" />);
@@ -516,11 +521,11 @@ describe("DataTable", () => {
516
521
  expect(screen.getByTestId("dt-sentinel").querySelector("svg")).not.toBeNull();
517
522
  });
518
523
 
519
- test("hasMore=false: 'End of list' Marker statt Sentinel-Wirkung", () => {
524
+ test("hasMore=false + viele Zeilen: 'End of list' Marker statt Sentinel-Wirkung", () => {
520
525
  render(
521
526
  <DataTable
522
527
  columns={cols}
523
- rows={oneRow}
528
+ rows={manyRows}
524
529
  testId="dt"
525
530
  onReachEnd={mock()}
526
531
  loadingMore={false}
@@ -531,6 +536,21 @@ describe("DataTable", () => {
531
536
  expect(screen.getByTestId("dt-sentinel-end").textContent).toContain("End of list");
532
537
  });
533
538
 
539
+ test("hasMore=false + kurze Liste: kein End-Marker (#1699)", () => {
540
+ render(
541
+ <DataTable
542
+ columns={cols}
543
+ rows={oneRow}
544
+ testId="dt"
545
+ onReachEnd={mock()}
546
+ loadingMore={false}
547
+ hasMore={false}
548
+ />,
549
+ );
550
+ expect(screen.queryByTestId("dt-sentinel-end")).toBeNull();
551
+ expect(screen.getByTestId("dt-sentinel").textContent).toBe("");
552
+ });
553
+
534
554
  test("hasMore=false + de-Locale: Marker kommt aus i18n statt hartcodiert", async () => {
535
555
  const { LocaleProvider, createStaticLocaleResolver, kumikoDefaultTranslations } =
536
556
  await import("@cosmicdrift/kumiko-renderer");
@@ -541,7 +561,7 @@ describe("DataTable", () => {
541
561
  >
542
562
  <DataTable
543
563
  columns={cols}
544
- rows={oneRow}
564
+ rows={manyRows}
545
565
  testId="dt"
546
566
  onReachEnd={mock()}
547
567
  loadingMore={false}
@@ -654,6 +654,7 @@ function DefaultDataTable({
654
654
  onReachEnd={onReachEnd}
655
655
  loadingMore={loadingMore === true}
656
656
  hasMore={hasMore !== false}
657
+ rowsCount={rows.length}
657
658
  testId={testId !== undefined ? `${testId}-sentinel` : "render-list-sentinel"}
658
659
  />
659
660
  </>
@@ -1018,15 +1019,23 @@ function RowActionsKebab({
1018
1019
  // lädt, ignorieren wir weitere Sichtbar-Events). Kein observer in
1019
1020
  // Server-Side-Render, kein observer wenn hasMore=false — dann zeigt
1020
1021
  // der Sentinel nur den End-of-list-Hinweis.
1022
+ // ponytail: End-Marker nur ab END_LABEL_MIN_ROWS Zeilen — eine kurze
1023
+ // Liste (eine einzige Page) zeigt ihr Ende selbst, da wäre der Marker
1024
+ // nur Lärm. Schwellwert = eine Page; per Prop übersteuerbar falls Apps
1025
+ // andere Page-Größen fahren.
1026
+ const END_LABEL_MIN_ROWS = 20;
1027
+
1021
1028
  function InfiniteSentinel({
1022
1029
  onReachEnd,
1023
1030
  loadingMore,
1024
1031
  hasMore,
1032
+ rowsCount,
1025
1033
  testId,
1026
1034
  }: {
1027
1035
  readonly onReachEnd: () => void;
1028
1036
  readonly loadingMore: boolean;
1029
1037
  readonly hasMore: boolean;
1038
+ readonly rowsCount: number;
1030
1039
  readonly testId?: string;
1031
1040
  }): ReactNode {
1032
1041
  const t = useTranslation();
@@ -1057,23 +1066,28 @@ function InfiniteSentinel({
1057
1066
  return () => observer.disconnect();
1058
1067
  }, [onReachEnd, loadingMore, hasMore]);
1059
1068
 
1069
+ const showEndLabel = !hasMore && rowsCount >= END_LABEL_MIN_ROWS;
1070
+
1060
1071
  return (
1061
1072
  <div
1062
1073
  ref={ref}
1063
1074
  data-testid={testId}
1064
- className="flex items-center justify-center py-4 text-sm text-muted-foreground"
1075
+ className={cn(
1076
+ "flex items-center justify-center text-sm text-muted-foreground",
1077
+ showEndLabel || loadingMore ? "py-4" : "",
1078
+ )}
1065
1079
  >
1066
- {!hasMore ? (
1080
+ {showEndLabel ? (
1067
1081
  <span data-testid={testId !== undefined ? `${testId}-end` : undefined}>
1068
1082
  {t("kumiko.list.end-of-list")}
1069
1083
  </span>
1070
1084
  ) : loadingMore ? (
1071
1085
  <Loader2 className="size-4 animate-spin" aria-hidden="true" />
1072
- ) : (
1073
- // Unsichtbar-Spacer wenn weder loading noch end — der Observer
1074
- // braucht ein DOM-Node, der User soll aber nichts sehen.
1086
+ ) : hasMore ? (
1087
+ // Unsichtbar-Spacer solange noch geladen werden kann — der
1088
+ // Observer braucht ein DOM-Node, der User soll aber nichts sehen.
1075
1089
  <span aria-hidden="true">&nbsp;</span>
1076
- )}
1090
+ ) : null}
1077
1091
  </div>
1078
1092
  );
1079
1093
  }