@zentauri-ui/zentauri-components 1.8.0 → 1.8.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/README.md +25 -10
- package/cli/registry.json +12 -0
- package/dist/charts/area.js +9 -10
- package/dist/charts/area.js.map +1 -1
- package/dist/charts/area.mjs +2 -3
- package/dist/charts/area.mjs.map +1 -1
- package/dist/charts/bar.js +10 -95
- package/dist/charts/bar.js.map +1 -1
- package/dist/charts/bar.mjs +2 -95
- package/dist/charts/bar.mjs.map +1 -1
- package/dist/charts/bubble.js +8 -9
- package/dist/charts/bubble.js.map +1 -1
- package/dist/charts/bubble.mjs +2 -3
- package/dist/charts/bubble.mjs.map +1 -1
- package/dist/charts/funnel/Funnel.d.ts +6 -0
- package/dist/charts/funnel/Funnel.d.ts.map +1 -0
- package/dist/charts/funnel/index.d.ts +4 -0
- package/dist/charts/funnel/index.d.ts.map +1 -0
- package/dist/charts/funnel.js +102 -0
- package/dist/charts/funnel.js.map +1 -0
- package/dist/charts/funnel.mjs +89 -0
- package/dist/charts/funnel.mjs.map +1 -0
- package/dist/charts/line.js +8 -9
- package/dist/charts/line.js.map +1 -1
- package/dist/charts/line.mjs +2 -3
- package/dist/charts/line.mjs.map +1 -1
- package/dist/charts/pie/Pie.d.ts +1 -1
- package/dist/charts/pie/Pie.d.ts.map +1 -1
- package/dist/charts/pie.js +19 -6
- package/dist/charts/pie.js.map +1 -1
- package/dist/charts/pie.mjs +17 -4
- package/dist/charts/pie.mjs.map +1 -1
- package/dist/charts/radar/Radar.d.ts +6 -0
- package/dist/charts/radar/Radar.d.ts.map +1 -0
- package/dist/charts/radar/index.d.ts +4 -0
- package/dist/charts/radar/index.d.ts.map +1 -0
- package/dist/charts/radar.js +94 -0
- package/dist/charts/radar.js.map +1 -0
- package/dist/charts/radar.mjs +81 -0
- package/dist/charts/radar.mjs.map +1 -0
- package/dist/charts/scatter/Scatter.d.ts +6 -0
- package/dist/charts/scatter/Scatter.d.ts.map +1 -0
- package/dist/charts/scatter/index.d.ts +4 -0
- package/dist/charts/scatter/index.d.ts.map +1 -0
- package/dist/charts/scatter.js +116 -0
- package/dist/charts/scatter.js.map +1 -0
- package/dist/charts/scatter.mjs +103 -0
- package/dist/charts/scatter.mjs.map +1 -0
- package/dist/charts/shared/chart-frame.d.ts +2 -1
- package/dist/charts/shared/chart-frame.d.ts.map +1 -1
- package/dist/charts/shared/types.d.ts +22 -2
- package/dist/charts/shared/types.d.ts.map +1 -1
- package/dist/charts/stacked-bar/StackedBar.d.ts +6 -0
- package/dist/charts/stacked-bar/StackedBar.d.ts.map +1 -0
- package/dist/charts/stacked-bar/index.d.ts +4 -0
- package/dist/charts/stacked-bar/index.d.ts.map +1 -0
- package/dist/charts/stacked-bar.js +29 -0
- package/dist/charts/stacked-bar.js.map +1 -0
- package/dist/charts/stacked-bar.mjs +15 -0
- package/dist/charts/stacked-bar.mjs.map +1 -0
- package/dist/chunk-7TGUGTTQ.mjs +147 -0
- package/dist/chunk-7TGUGTTQ.mjs.map +1 -0
- package/dist/chunk-CQMV7BB6.js +50 -0
- package/dist/chunk-CQMV7BB6.js.map +1 -0
- package/dist/chunk-DN7TYUJ6.js +119 -0
- package/dist/chunk-DN7TYUJ6.js.map +1 -0
- package/dist/chunk-F3V4POW3.mjs +8 -0
- package/dist/chunk-F3V4POW3.mjs.map +1 -0
- package/dist/{chunk-G2WARVAM.mjs → chunk-HZIRD3SR.mjs} +35 -15
- package/dist/chunk-HZIRD3SR.mjs.map +1 -0
- package/dist/{chunk-G66SXATZ.js → chunk-IL4LH2XX.js} +50 -4
- package/dist/chunk-IL4LH2XX.js.map +1 -0
- package/dist/chunk-LREMK2XR.js +97 -0
- package/dist/chunk-LREMK2XR.js.map +1 -0
- package/dist/chunk-O2KM3ETC.mjs +95 -0
- package/dist/chunk-O2KM3ETC.mjs.map +1 -0
- package/dist/chunk-ODBG4Y6R.mjs +48 -0
- package/dist/chunk-ODBG4Y6R.mjs.map +1 -0
- package/dist/{chunk-ZIFMIS7D.mjs → chunk-OL3BJSRC.mjs} +51 -5
- package/dist/chunk-OL3BJSRC.mjs.map +1 -0
- package/dist/{chunk-QNUDODDX.js → chunk-PWPMKXEG.js} +36 -14
- package/dist/chunk-PWPMKXEG.js.map +1 -0
- package/dist/chunk-RKX5MERK.js +150 -0
- package/dist/chunk-RKX5MERK.js.map +1 -0
- package/dist/chunk-VYI3GS2C.mjs +115 -0
- package/dist/chunk-VYI3GS2C.mjs.map +1 -0
- package/dist/chunk-XRM7GOIE.js +10 -0
- package/dist/chunk-XRM7GOIE.js.map +1 -0
- package/dist/design-system/copy-button.d.ts +43 -0
- package/dist/design-system/copy-button.d.ts.map +1 -0
- package/dist/design-system/index.d.ts +2 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/kbd.d.ts +44 -0
- package/dist/design-system/kbd.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useClipboard.js +6 -44
- package/dist/hooks/useClipboard.js.map +1 -1
- package/dist/hooks/useClipboard.mjs +1 -46
- package/dist/hooks/useClipboard.mjs.map +1 -1
- package/dist/hooks/useIsomorphicLayoutEffect.js +6 -4
- package/dist/hooks/useIsomorphicLayoutEffect.js.map +1 -1
- package/dist/hooks/useIsomorphicLayoutEffect.mjs +1 -6
- package/dist/hooks/useIsomorphicLayoutEffect.mjs.map +1 -1
- package/dist/hooks/useTableFilter/index.d.ts +3 -0
- package/dist/hooks/useTableFilter/index.d.ts.map +1 -0
- package/dist/hooks/useTableFilter/types.d.ts +20 -0
- package/dist/hooks/useTableFilter/types.d.ts.map +1 -0
- package/dist/hooks/useTableFilter/useTableFilter.d.ts +3 -0
- package/dist/hooks/useTableFilter/useTableFilter.d.ts.map +1 -0
- package/dist/hooks/useTableFilter.js +124 -0
- package/dist/hooks/useTableFilter.js.map +1 -0
- package/dist/hooks/useTableFilter.mjs +122 -0
- package/dist/hooks/useTableFilter.mjs.map +1 -0
- package/dist/hooks/useTableSort/index.d.ts +3 -0
- package/dist/hooks/useTableSort/index.d.ts.map +1 -0
- package/dist/hooks/useTableSort/types.d.ts +15 -0
- package/dist/hooks/useTableSort/types.d.ts.map +1 -0
- package/dist/hooks/useTableSort/useTableSort.d.ts +3 -0
- package/dist/hooks/useTableSort/useTableSort.d.ts.map +1 -0
- package/dist/hooks/useTableSort.js +99 -0
- package/dist/hooks/useTableSort.js.map +1 -0
- package/dist/hooks/useTableSort.mjs +97 -0
- package/dist/hooks/useTableSort.mjs.map +1 -0
- package/dist/ui/copy-button/animated/animations.d.ts +3 -0
- package/dist/ui/copy-button/animated/animations.d.ts.map +1 -0
- package/dist/ui/copy-button/animated/copy-button-animated.d.ts +6 -0
- package/dist/ui/copy-button/animated/copy-button-animated.d.ts.map +1 -0
- package/dist/ui/copy-button/animated/index.d.ts +4 -0
- package/dist/ui/copy-button/animated/index.d.ts.map +1 -0
- package/dist/ui/copy-button/animated/types.d.ts +26 -0
- package/dist/ui/copy-button/animated/types.d.ts.map +1 -0
- package/dist/ui/copy-button/animated.js +59 -0
- package/dist/ui/copy-button/animated.js.map +1 -0
- package/dist/ui/copy-button/animated.mjs +56 -0
- package/dist/ui/copy-button/animated.mjs.map +1 -0
- package/dist/ui/copy-button/copy-button-base.d.ts +6 -0
- package/dist/ui/copy-button/copy-button-base.d.ts.map +1 -0
- package/dist/ui/copy-button/copy-button.d.ts +6 -0
- package/dist/ui/copy-button/copy-button.d.ts.map +1 -0
- package/dist/ui/copy-button/index.d.ts +4 -0
- package/dist/ui/copy-button/index.d.ts.map +1 -0
- package/dist/ui/copy-button/types.d.ts +32 -0
- package/dist/ui/copy-button/types.d.ts.map +1 -0
- package/dist/ui/copy-button/variants.d.ts +6 -0
- package/dist/ui/copy-button/variants.d.ts.map +1 -0
- package/dist/ui/copy-button.js +20 -0
- package/dist/ui/copy-button.js.map +1 -0
- package/dist/ui/copy-button.mjs +15 -0
- package/dist/ui/copy-button.mjs.map +1 -0
- package/dist/ui/kbd/animated/animations.d.ts +3 -0
- package/dist/ui/kbd/animated/animations.d.ts.map +1 -0
- package/dist/ui/kbd/animated/index.d.ts +4 -0
- package/dist/ui/kbd/animated/index.d.ts.map +1 -0
- package/dist/ui/kbd/animated/kbd-animated.d.ts +6 -0
- package/dist/ui/kbd/animated/kbd-animated.d.ts.map +1 -0
- package/dist/ui/kbd/animated/types.d.ts +10 -0
- package/dist/ui/kbd/animated/types.d.ts.map +1 -0
- package/dist/ui/kbd/animated.js +42 -0
- package/dist/ui/kbd/animated.js.map +1 -0
- package/dist/ui/kbd/animated.mjs +39 -0
- package/dist/ui/kbd/animated.mjs.map +1 -0
- package/dist/ui/kbd/index.d.ts +4 -0
- package/dist/ui/kbd/index.d.ts.map +1 -0
- package/dist/ui/kbd/kbd-base.d.ts +6 -0
- package/dist/ui/kbd/kbd-base.d.ts.map +1 -0
- package/dist/ui/kbd/kbd.d.ts +6 -0
- package/dist/ui/kbd/kbd.d.ts.map +1 -0
- package/dist/ui/kbd/types.d.ts +17 -0
- package/dist/ui/kbd/types.d.ts.map +1 -0
- package/dist/ui/kbd/variants.d.ts +8 -0
- package/dist/ui/kbd/variants.d.ts.map +1 -0
- package/dist/ui/kbd.js +23 -0
- package/dist/ui/kbd.js.map +1 -0
- package/dist/ui/kbd.mjs +14 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/marquee/marquee.d.ts.map +1 -1
- package/dist/ui/marquee.js +82 -21
- package/dist/ui/marquee.js.map +1 -1
- package/dist/ui/marquee.mjs +83 -22
- package/dist/ui/marquee.mjs.map +1 -1
- package/dist/ui/table/animated.js +8 -8
- package/dist/ui/table/animated.mjs +2 -2
- package/dist/ui/table/index.d.ts +1 -1
- package/dist/ui/table/index.d.ts.map +1 -1
- package/dist/ui/table/table-base.d.ts +2 -2
- package/dist/ui/table/table-base.d.ts.map +1 -1
- package/dist/ui/table/types.d.ts +9 -1
- package/dist/ui/table/types.d.ts.map +1 -1
- package/dist/ui/table.js +14 -14
- package/dist/ui/table.mjs +1 -1
- package/package.json +1 -1
- package/src/charts/charts.test.tsx +80 -0
- package/src/charts/funnel/Funnel.tsx +105 -0
- package/src/charts/funnel/index.ts +14 -0
- package/src/charts/pie/Pie.tsx +28 -1
- package/src/charts/radar/Radar.tsx +84 -0
- package/src/charts/radar/index.ts +16 -0
- package/src/charts/scatter/Scatter.tsx +104 -0
- package/src/charts/scatter/index.ts +16 -0
- package/src/charts/shared/chart-frame.tsx +4 -2
- package/src/charts/shared/types.ts +42 -2
- package/src/charts/stacked-bar/StackedBar.tsx +12 -0
- package/src/charts/stacked-bar/index.ts +16 -0
- package/src/design-system/copy-button.ts +81 -0
- package/src/design-system/index.ts +2 -0
- package/src/design-system/kbd.ts +83 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useTableFilter/index.ts +7 -0
- package/src/hooks/useTableFilter/types.ts +28 -0
- package/src/hooks/useTableFilter/useTableFilter.test.ts +141 -0
- package/src/hooks/useTableFilter/useTableFilter.ts +153 -0
- package/src/hooks/useTableSort/index.ts +5 -0
- package/src/hooks/useTableSort/types.ts +23 -0
- package/src/hooks/useTableSort/useTableSort.test.ts +150 -0
- package/src/hooks/useTableSort/useTableSort.ts +121 -0
- package/src/ui/copy-button/animated/animations.ts +22 -0
- package/src/ui/copy-button/animated/copy-button-animated.tsx +39 -0
- package/src/ui/copy-button/animated/index.ts +10 -0
- package/src/ui/copy-button/animated/types.ts +21 -0
- package/src/ui/copy-button/copy-button-base.tsx +88 -0
- package/src/ui/copy-button/copy-button.test.tsx +82 -0
- package/src/ui/copy-button/copy-button.tsx +9 -0
- package/src/ui/copy-button/index.ts +10 -0
- package/src/ui/copy-button/types.ts +37 -0
- package/src/ui/copy-button/variants.ts +29 -0
- package/src/ui/divider/divider.test.tsx +55 -0
- package/src/ui/empty-state/empty-state.test.tsx +88 -0
- package/src/ui/kbd/animated/animations.ts +15 -0
- package/src/ui/kbd/animated/index.ts +9 -0
- package/src/ui/kbd/animated/kbd-animated.tsx +26 -0
- package/src/ui/kbd/animated/types.ts +16 -0
- package/src/ui/kbd/index.ts +5 -0
- package/src/ui/kbd/kbd-base.tsx +50 -0
- package/src/ui/kbd/kbd.test.tsx +48 -0
- package/src/ui/kbd/kbd.tsx +9 -0
- package/src/ui/kbd/types.ts +21 -0
- package/src/ui/kbd/variants.ts +31 -0
- package/src/ui/marquee/marquee.test.tsx +45 -4
- package/src/ui/marquee/marquee.tsx +100 -18
- package/src/ui/skeleton/skeleton.test.tsx +85 -0
- package/src/ui/table/index.ts +3 -0
- package/src/ui/table/table-base.tsx +69 -4
- package/src/ui/table/table.test.tsx +207 -0
- package/src/ui/table/types.ts +13 -1
- package/dist/chunk-G2WARVAM.mjs.map +0 -1
- package/dist/chunk-G66SXATZ.js.map +0 -1
- package/dist/chunk-OULU7OC4.mjs +0 -21
- package/dist/chunk-OULU7OC4.mjs.map +0 -1
- package/dist/chunk-QNUDODDX.js.map +0 -1
- package/dist/chunk-Z6S36PDD.js +0 -24
- package/dist/chunk-Z6S36PDD.js.map +0 -1
- package/dist/chunk-ZIFMIS7D.mjs.map +0 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { Marquee } from "./marquee";
|
|
5
5
|
|
|
6
6
|
describe("Marquee", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
7
11
|
it("exposes a display name", () => {
|
|
8
12
|
expect(Marquee.displayName).toBe("Marquee");
|
|
9
13
|
});
|
|
@@ -17,13 +21,14 @@ describe("Marquee", () => {
|
|
|
17
21
|
|
|
18
22
|
expect(document.querySelector('[data-slot="marquee"]')).toBeTruthy();
|
|
19
23
|
expect(document.querySelector('[data-slot="marquee-track"]')).toBeTruthy();
|
|
20
|
-
expect(screen.getAllByText("Acme")).toHaveLength(2);
|
|
21
|
-
|
|
22
24
|
const groups = document.querySelectorAll(
|
|
23
25
|
'[data-slot="marquee-item-group"]',
|
|
24
26
|
);
|
|
27
|
+
expect(groups).toHaveLength(2);
|
|
28
|
+
expect(groups[0]?.textContent).toContain("Acme");
|
|
25
29
|
expect(groups[1]).toHaveAttribute("aria-hidden", "true");
|
|
26
30
|
expect(groups[1]).toHaveAttribute("inert");
|
|
31
|
+
expect(groups[1]?.textContent).toContain("Acme");
|
|
27
32
|
});
|
|
28
33
|
|
|
29
34
|
it("applies horizontal metadata by default", () => {
|
|
@@ -84,6 +89,42 @@ describe("Marquee", () => {
|
|
|
84
89
|
});
|
|
85
90
|
});
|
|
86
91
|
|
|
92
|
+
it("hides filler copies from assistive technology", async () => {
|
|
93
|
+
vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(
|
|
94
|
+
function (this: HTMLElement) {
|
|
95
|
+
return this.getAttribute("data-slot") === "marquee" ? 300 : 0;
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockImplementation(
|
|
99
|
+
function (this: HTMLElement) {
|
|
100
|
+
return this.getAttribute("data-slot") === "marquee-measure" ? 100 : 0;
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
render(
|
|
105
|
+
<Marquee>
|
|
106
|
+
<button type="button">Focusable item</button>
|
|
107
|
+
</Marquee>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const firstGroup = document.querySelector(
|
|
111
|
+
'[data-slot="marquee-item-group"]',
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(firstGroup?.textContent).toBe(
|
|
116
|
+
"Focusable itemFocusable itemFocusable item",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
screen.getAllByRole("button", { name: "Focusable item" }),
|
|
122
|
+
).toHaveLength(1);
|
|
123
|
+
expect(
|
|
124
|
+
firstGroup?.querySelectorAll('[aria-hidden="true"][inert]'),
|
|
125
|
+
).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
87
128
|
it("accepts string gap values", () => {
|
|
88
129
|
render(
|
|
89
130
|
<Marquee gap="2rem" data-testid="marquee">
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { Children, useCallback, useMemo, useRef, useState } from "react";
|
|
4
|
+
import type { CSSProperties, Ref } from "react";
|
|
4
5
|
|
|
6
|
+
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
|
|
5
7
|
import { cn } from "../../lib/utils";
|
|
6
8
|
|
|
7
9
|
import type { MarqueeProps } from "./types";
|
|
@@ -16,6 +18,17 @@ function toCssLength(value: number | string | undefined) {
|
|
|
16
18
|
return typeof value === "number" ? `${value}px` : value;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
function assignRef<TElement>(
|
|
22
|
+
ref: Ref<TElement> | undefined,
|
|
23
|
+
value: TElement | null,
|
|
24
|
+
) {
|
|
25
|
+
if (typeof ref === "function") {
|
|
26
|
+
ref(value);
|
|
27
|
+
} else if (ref) {
|
|
28
|
+
(ref as { current: TElement | null }).current = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
export function Marquee(props: MarqueeProps) {
|
|
20
33
|
const {
|
|
21
34
|
appearance,
|
|
@@ -44,14 +57,81 @@ export function Marquee(props: MarqueeProps) {
|
|
|
44
57
|
resolvedDirection === "right" || resolvedDirection === "down";
|
|
45
58
|
const animationName =
|
|
46
59
|
resolvedOrientation === "vertical" ? "zui-marquee-y" : "zui-marquee-x";
|
|
60
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
61
|
+
const measureRef = useRef<HTMLDivElement | null>(null);
|
|
62
|
+
const [copyCount, setCopyCount] = useState(1);
|
|
63
|
+
const childArray = useMemo(() => Children.toArray(children), [children]);
|
|
64
|
+
const setRootRef = useCallback(
|
|
65
|
+
(node: HTMLDivElement | null) => {
|
|
66
|
+
rootRef.current = node;
|
|
67
|
+
assignRef(ref, node);
|
|
68
|
+
},
|
|
69
|
+
[ref],
|
|
70
|
+
);
|
|
47
71
|
const marqueeStyle = {
|
|
48
72
|
...(gap !== undefined ? { "--zui-marquee-gap": toCssLength(gap) } : null),
|
|
49
73
|
...style,
|
|
50
74
|
} as CSSProperties;
|
|
75
|
+
const groupClassName = cn(
|
|
76
|
+
"flex shrink-0 items-center justify-around gap-(--zui-marquee-gap)",
|
|
77
|
+
resolvedOrientation === "vertical" ? "flex-col" : "flex-row",
|
|
78
|
+
itemClassName,
|
|
79
|
+
);
|
|
80
|
+
const fillerChildren = Array.from(
|
|
81
|
+
{ length: Math.max(0, copyCount - 1) },
|
|
82
|
+
(_, index) => (
|
|
83
|
+
<div key={index} aria-hidden="true" inert className="contents">
|
|
84
|
+
{childArray}
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
useIsomorphicLayoutEffect(() => {
|
|
90
|
+
const updateCopyCount = () => {
|
|
91
|
+
const root = rootRef.current;
|
|
92
|
+
const measure = measureRef.current;
|
|
93
|
+
|
|
94
|
+
if (!root || !measure) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rootSize =
|
|
99
|
+
resolvedOrientation === "vertical"
|
|
100
|
+
? root.offsetHeight
|
|
101
|
+
: root.offsetWidth;
|
|
102
|
+
const contentSize =
|
|
103
|
+
resolvedOrientation === "vertical"
|
|
104
|
+
? measure.scrollHeight
|
|
105
|
+
: measure.scrollWidth;
|
|
106
|
+
|
|
107
|
+
if (!rootSize || !contentSize) {
|
|
108
|
+
setCopyCount(1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setCopyCount(Math.max(1, Math.ceil(rootSize / contentSize)));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
updateCopyCount();
|
|
116
|
+
|
|
117
|
+
if (typeof ResizeObserver === "undefined") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const observer = new ResizeObserver(updateCopyCount);
|
|
122
|
+
if (rootRef.current) {
|
|
123
|
+
observer.observe(rootRef.current);
|
|
124
|
+
}
|
|
125
|
+
if (measureRef.current) {
|
|
126
|
+
observer.observe(measureRef.current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return () => observer.disconnect();
|
|
130
|
+
}, [childArray, gap, resolvedOrientation]);
|
|
51
131
|
|
|
52
132
|
return (
|
|
53
133
|
<div
|
|
54
|
-
ref={
|
|
134
|
+
ref={setRootRef}
|
|
55
135
|
data-direction={resolvedDirection}
|
|
56
136
|
data-orientation={resolvedOrientation}
|
|
57
137
|
data-slot="marquee"
|
|
@@ -68,10 +148,21 @@ export function Marquee(props: MarqueeProps) {
|
|
|
68
148
|
{...rest}
|
|
69
149
|
>
|
|
70
150
|
<style>{marqueeKeyframes}</style>
|
|
151
|
+
<div
|
|
152
|
+
aria-hidden="true"
|
|
153
|
+
data-slot="marquee-measure"
|
|
154
|
+
ref={measureRef}
|
|
155
|
+
className={cn(
|
|
156
|
+
"pointer-events-none invisible absolute -z-10",
|
|
157
|
+
groupClassName,
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{childArray}
|
|
161
|
+
</div>
|
|
71
162
|
<div
|
|
72
163
|
data-slot="marquee-track"
|
|
73
164
|
className={cn(
|
|
74
|
-
"flex shrink-0 gap-
|
|
165
|
+
"flex shrink-0 gap-(--zui-marquee-gap) will-change-transform [animation-iteration-count:infinite] [animation-timing-function:linear] motion-reduce:[animation-play-state:paused]",
|
|
75
166
|
resolvedOrientation === "vertical" ? "flex-col" : "w-max flex-row",
|
|
76
167
|
pauseOnHover && "group-hover/marquee:[animation-play-state:paused]",
|
|
77
168
|
isReverse && "[animation-direction:reverse]",
|
|
@@ -84,27 +175,18 @@ export function Marquee(props: MarqueeProps) {
|
|
|
84
175
|
} as CSSProperties
|
|
85
176
|
}
|
|
86
177
|
>
|
|
87
|
-
<div
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"flex shrink-0 items-center justify-around gap-[var(--zui-marquee-gap)]",
|
|
91
|
-
resolvedOrientation === "vertical" ? "flex-col" : "flex-row",
|
|
92
|
-
itemClassName,
|
|
93
|
-
)}
|
|
94
|
-
>
|
|
95
|
-
{children}
|
|
178
|
+
<div data-slot="marquee-item-group" className={groupClassName}>
|
|
179
|
+
{childArray}
|
|
180
|
+
{fillerChildren}
|
|
96
181
|
</div>
|
|
97
182
|
<div
|
|
98
183
|
aria-hidden="true"
|
|
99
184
|
inert
|
|
100
185
|
data-slot="marquee-item-group"
|
|
101
|
-
className={
|
|
102
|
-
"flex shrink-0 items-center justify-around gap-[var(--zui-marquee-gap)]",
|
|
103
|
-
resolvedOrientation === "vertical" ? "flex-col" : "flex-row",
|
|
104
|
-
itemClassName,
|
|
105
|
-
)}
|
|
186
|
+
className={groupClassName}
|
|
106
187
|
>
|
|
107
|
-
{
|
|
188
|
+
{childArray}
|
|
189
|
+
{fillerChildren}
|
|
108
190
|
</div>
|
|
109
191
|
</div>
|
|
110
192
|
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Skeleton,
|
|
7
|
+
SkeletonAvatar,
|
|
8
|
+
SkeletonButton,
|
|
9
|
+
SkeletonCard,
|
|
10
|
+
SkeletonText,
|
|
11
|
+
} from "./skeleton";
|
|
12
|
+
|
|
13
|
+
describe("Skeleton", () => {
|
|
14
|
+
it("should set displayName on skeleton primitives", () => {
|
|
15
|
+
expect(Skeleton.displayName).toBe("Skeleton");
|
|
16
|
+
expect(SkeletonText.displayName).toBe("SkeletonText");
|
|
17
|
+
expect(SkeletonAvatar.displayName).toBe("SkeletonAvatar");
|
|
18
|
+
expect(SkeletonCard.displayName).toBe("SkeletonCard");
|
|
19
|
+
expect(SkeletonButton.displayName).toBe("SkeletonButton");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should stamp data-slot and hide the placeholder from assistive tech", () => {
|
|
23
|
+
const { container } = render(<Skeleton busy />);
|
|
24
|
+
const root = container.querySelector('[data-slot="skeleton"]');
|
|
25
|
+
expect(root).toHaveAttribute("aria-hidden", "true");
|
|
26
|
+
expect(root).toHaveAttribute("aria-busy", "true");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should apply pulse motion when animation is not none", () => {
|
|
30
|
+
const { container } = render(<Skeleton animation="pulse" />);
|
|
31
|
+
expect(
|
|
32
|
+
container.querySelector('[data-slot="skeleton"]')?.className,
|
|
33
|
+
).toMatch(/animate-pulse/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should omit motion classes when animation is none", () => {
|
|
37
|
+
const { container } = render(<Skeleton animation="none" />);
|
|
38
|
+
expect(
|
|
39
|
+
container.querySelector('[data-slot="skeleton"]')?.className,
|
|
40
|
+
).not.toMatch(/animate-pulse/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should render the requested number of text lines", () => {
|
|
44
|
+
const { container } = render(<SkeletonText lines={4} />);
|
|
45
|
+
const root = container.querySelector('[data-slot="skeleton-text"]');
|
|
46
|
+
expect(root).toHaveAttribute("aria-hidden", "true");
|
|
47
|
+
expect(root?.children.length).toBe(4);
|
|
48
|
+
expect(root?.lastElementChild?.className).toMatch(/w-3\/5/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should share parent animation with nested text placeholders", () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<Skeleton animation="none">
|
|
54
|
+
<SkeletonText lines={2} />
|
|
55
|
+
</Skeleton>,
|
|
56
|
+
);
|
|
57
|
+
const text = container.querySelector('[data-slot="skeleton-text"]');
|
|
58
|
+
expect(text?.firstElementChild?.className).not.toMatch(/animate-pulse/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should render avatar, button, and card slots", () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<>
|
|
64
|
+
<SkeletonAvatar avatarSize="xl" />
|
|
65
|
+
<SkeletonButton buttonSize="lg" />
|
|
66
|
+
<SkeletonCard />
|
|
67
|
+
</>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
container.querySelector('[data-slot="skeleton-avatar"]'),
|
|
72
|
+
).toHaveClass("size-14");
|
|
73
|
+
expect(
|
|
74
|
+
container.querySelector('[data-slot="skeleton-button"]'),
|
|
75
|
+
).toHaveClass("h-12");
|
|
76
|
+
expect(container.querySelector('[data-slot="skeleton-card"]')).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should forward refs to the base skeleton element", () => {
|
|
80
|
+
const ref = createRef<HTMLDivElement>();
|
|
81
|
+
render(<Skeleton ref={ref} />);
|
|
82
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
83
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("skeleton");
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/ui/table/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo,
|
|
8
|
+
type KeyboardEvent,
|
|
9
|
+
} from "react";
|
|
4
10
|
|
|
5
11
|
import { cn } from "../../lib/utils";
|
|
6
12
|
|
|
@@ -10,6 +16,7 @@ import type {
|
|
|
10
16
|
TableHeadCellProps,
|
|
11
17
|
TableProps,
|
|
12
18
|
TableSectionProps,
|
|
19
|
+
TableSortDirection,
|
|
13
20
|
} from "./types";
|
|
14
21
|
import { tableCellVariants, tableRowVariants, tableVariants } from "./variants";
|
|
15
22
|
|
|
@@ -122,19 +129,20 @@ export function TableRow({
|
|
|
122
129
|
children,
|
|
123
130
|
ref,
|
|
124
131
|
as: Wrapper = "tr",
|
|
132
|
+
rowAnimation: _rowAnimation,
|
|
125
133
|
...rest
|
|
126
134
|
}: TableSectionProps & { ref?: React.Ref<HTMLTableRowElement> }) {
|
|
127
135
|
const { appearance } = useTableContext("TableRow");
|
|
128
136
|
|
|
129
137
|
return (
|
|
130
|
-
<
|
|
138
|
+
<Wrapper
|
|
131
139
|
ref={ref}
|
|
132
140
|
data-slot="table-row"
|
|
133
141
|
className={cn(tableRowVariants({ appearance }), className)}
|
|
134
142
|
{...rest}
|
|
135
143
|
>
|
|
136
144
|
{children}
|
|
137
|
-
</
|
|
145
|
+
</Wrapper>
|
|
138
146
|
);
|
|
139
147
|
}
|
|
140
148
|
|
|
@@ -144,21 +152,78 @@ export function TableHead({
|
|
|
144
152
|
className,
|
|
145
153
|
children,
|
|
146
154
|
scope = "col",
|
|
155
|
+
sortKey,
|
|
147
156
|
sortDirection,
|
|
157
|
+
onSortChange,
|
|
158
|
+
onClick,
|
|
159
|
+
onKeyDown,
|
|
160
|
+
tabIndex,
|
|
148
161
|
ref,
|
|
149
162
|
...rest
|
|
150
163
|
}: TableHeadCellProps) {
|
|
151
164
|
const { appearance, size, textAlign } = useTableContext("TableHead");
|
|
165
|
+
const isSortable = Boolean(sortKey && onSortChange);
|
|
166
|
+
const sortableDirection: TableSortDirection = sortDirection ?? "none";
|
|
167
|
+
|
|
168
|
+
const handleSort = useCallback(() => {
|
|
169
|
+
if (!sortKey || !onSortChange) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const nextDirection: TableSortDirection =
|
|
174
|
+
sortableDirection === "ascending"
|
|
175
|
+
? "descending"
|
|
176
|
+
: sortableDirection === "descending"
|
|
177
|
+
? "none"
|
|
178
|
+
: "ascending";
|
|
179
|
+
|
|
180
|
+
onSortChange({
|
|
181
|
+
sortKey,
|
|
182
|
+
sortDirection: nextDirection,
|
|
183
|
+
});
|
|
184
|
+
}, [onSortChange, sortKey, sortableDirection]);
|
|
185
|
+
|
|
186
|
+
const handleClick = useCallback<NonNullable<TableHeadCellProps["onClick"]>>(
|
|
187
|
+
(event) => {
|
|
188
|
+
onClick?.(event);
|
|
189
|
+
if (!event.defaultPrevented) {
|
|
190
|
+
handleSort();
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
[handleSort, onClick],
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const handleKeyDown = useCallback(
|
|
197
|
+
(event: KeyboardEvent<HTMLTableCellElement>) => {
|
|
198
|
+
onKeyDown?.(event);
|
|
199
|
+
if (event.defaultPrevented || !isSortable) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
handleSort();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
[handleSort, isSortable, onKeyDown],
|
|
208
|
+
);
|
|
209
|
+
|
|
152
210
|
return (
|
|
153
211
|
<th
|
|
154
212
|
ref={ref}
|
|
155
213
|
data-slot="table-head"
|
|
156
214
|
scope={scope}
|
|
157
|
-
aria-sort={sortDirection}
|
|
215
|
+
aria-sort={isSortable ? sortableDirection : sortDirection}
|
|
216
|
+
data-sort-key={sortKey}
|
|
217
|
+
data-sort-direction={sortDirection}
|
|
218
|
+
tabIndex={isSortable ? (tabIndex ?? 0) : tabIndex}
|
|
158
219
|
className={cn(
|
|
159
220
|
tableCellVariants({ appearance, size, textAlign }),
|
|
221
|
+
isSortable &&
|
|
222
|
+
"cursor-pointer select-none outline-none focus-visible:ring-2 focus-visible:ring-slate-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950",
|
|
160
223
|
className,
|
|
161
224
|
)}
|
|
225
|
+
onClick={isSortable ? handleClick : onClick}
|
|
226
|
+
onKeyDown={isSortable ? handleKeyDown : onKeyDown}
|
|
162
227
|
{...rest}
|
|
163
228
|
>
|
|
164
229
|
{children}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { Table } from "./table";
|
|
7
|
+
import {
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCaption,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableFooter,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
} from "./table-base";
|
|
16
|
+
|
|
17
|
+
function renderBasicTable() {
|
|
18
|
+
return render(
|
|
19
|
+
<Table>
|
|
20
|
+
<TableCaption>Quarterly revenue</TableCaption>
|
|
21
|
+
<TableHeader>
|
|
22
|
+
<TableRow>
|
|
23
|
+
<TableHead>Company</TableHead>
|
|
24
|
+
<TableHead>ARR</TableHead>
|
|
25
|
+
</TableRow>
|
|
26
|
+
</TableHeader>
|
|
27
|
+
<TableBody>
|
|
28
|
+
<TableRow>
|
|
29
|
+
<TableCell scope="row">Zentauri</TableCell>
|
|
30
|
+
<TableCell>$42k</TableCell>
|
|
31
|
+
</TableRow>
|
|
32
|
+
</TableBody>
|
|
33
|
+
<TableFooter>
|
|
34
|
+
<TableRow>
|
|
35
|
+
<TableCell>Total</TableCell>
|
|
36
|
+
<TableCell>$42k</TableCell>
|
|
37
|
+
</TableRow>
|
|
38
|
+
</TableFooter>
|
|
39
|
+
</Table>,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("Table", () => {
|
|
44
|
+
it("should set displayName on compound parts", () => {
|
|
45
|
+
expect(Table.displayName).toBe("Table");
|
|
46
|
+
expect(TableHeader.displayName).toBe("TableHeader");
|
|
47
|
+
expect(TableBody.displayName).toBe("TableBody");
|
|
48
|
+
expect(TableFooter.displayName).toBe("TableFooter");
|
|
49
|
+
expect(TableRow.displayName).toBe("TableRow");
|
|
50
|
+
expect(TableHead.displayName).toBe("TableHead");
|
|
51
|
+
expect(TableCell.displayName).toBe("TableCell");
|
|
52
|
+
expect(TableCaption.displayName).toBe("TableCaption");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should render semantic table structure inside a focusable scroll region", () => {
|
|
56
|
+
const { container } = renderBasicTable();
|
|
57
|
+
expect(
|
|
58
|
+
screen.getByRole("region", { name: "Scrollable table" }),
|
|
59
|
+
).toHaveAttribute("data-slot", "table-scroll");
|
|
60
|
+
expect(screen.getByRole("table")).toHaveAttribute("data-slot", "table");
|
|
61
|
+
expect(
|
|
62
|
+
container.querySelector('[data-slot="table-caption"]'),
|
|
63
|
+
).toHaveTextContent("Quarterly revenue");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should support a custom scroll region label", () => {
|
|
67
|
+
render(<Table scrollAreaAriaLabel="Usage table" />);
|
|
68
|
+
expect(screen.getByRole("region", { name: "Usage table" })).toBeVisible();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should apply sticky header classes when stickyHeader is enabled", () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<Table stickyHeader>
|
|
74
|
+
<TableHeader>
|
|
75
|
+
<TableRow>
|
|
76
|
+
<TableHead>Name</TableHead>
|
|
77
|
+
</TableRow>
|
|
78
|
+
</TableHeader>
|
|
79
|
+
</Table>,
|
|
80
|
+
);
|
|
81
|
+
expect(
|
|
82
|
+
container.querySelector('[data-slot="table-header"]')?.className,
|
|
83
|
+
).toMatch(/sticky/);
|
|
84
|
+
expect(container.querySelector('[data-slot="table"]')?.className).toMatch(
|
|
85
|
+
/table-auto/,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should expose sort state on sortable header cells", () => {
|
|
90
|
+
render(
|
|
91
|
+
<Table>
|
|
92
|
+
<TableHeader>
|
|
93
|
+
<TableRow>
|
|
94
|
+
<TableHead sortKey="name" sortDirection="ascending">
|
|
95
|
+
Name
|
|
96
|
+
</TableHead>
|
|
97
|
+
</TableRow>
|
|
98
|
+
</TableHeader>
|
|
99
|
+
</Table>,
|
|
100
|
+
);
|
|
101
|
+
const header = screen.getByRole("columnheader", { name: "Name" });
|
|
102
|
+
expect(header).toHaveAttribute("aria-sort", "ascending");
|
|
103
|
+
expect(header).toHaveAttribute("data-sort-key", "name");
|
|
104
|
+
expect(header).toHaveAttribute("data-sort-direction", "ascending");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should mark sortable unsorted header cells with aria-sort none", () => {
|
|
108
|
+
render(
|
|
109
|
+
<Table>
|
|
110
|
+
<TableHeader>
|
|
111
|
+
<TableRow>
|
|
112
|
+
<TableHead sortKey="name" onSortChange={vi.fn()}>
|
|
113
|
+
Name
|
|
114
|
+
</TableHead>
|
|
115
|
+
</TableRow>
|
|
116
|
+
</TableHeader>
|
|
117
|
+
</Table>,
|
|
118
|
+
);
|
|
119
|
+
expect(screen.getByRole("columnheader", { name: "Name" })).toHaveAttribute(
|
|
120
|
+
"aria-sort",
|
|
121
|
+
"none",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should call onSortChange with the next direction on click", async () => {
|
|
126
|
+
const user = userEvent.setup();
|
|
127
|
+
const handleSortChange = vi.fn();
|
|
128
|
+
render(
|
|
129
|
+
<Table>
|
|
130
|
+
<TableHeader>
|
|
131
|
+
<TableRow>
|
|
132
|
+
<TableHead
|
|
133
|
+
sortKey="name"
|
|
134
|
+
sortDirection="ascending"
|
|
135
|
+
onSortChange={handleSortChange}
|
|
136
|
+
>
|
|
137
|
+
Name
|
|
138
|
+
</TableHead>
|
|
139
|
+
</TableRow>
|
|
140
|
+
</TableHeader>
|
|
141
|
+
</Table>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await user.click(screen.getByRole("columnheader", { name: "Name" }));
|
|
145
|
+
expect(handleSortChange).toHaveBeenCalledWith({
|
|
146
|
+
sortKey: "name",
|
|
147
|
+
sortDirection: "descending",
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should support keyboard sorting", async () => {
|
|
152
|
+
const user = userEvent.setup();
|
|
153
|
+
const handleSortChange = vi.fn();
|
|
154
|
+
render(
|
|
155
|
+
<Table>
|
|
156
|
+
<TableHeader>
|
|
157
|
+
<TableRow>
|
|
158
|
+
<TableHead
|
|
159
|
+
sortKey="createdAt"
|
|
160
|
+
sortDirection="none"
|
|
161
|
+
onSortChange={handleSortChange}
|
|
162
|
+
>
|
|
163
|
+
Created
|
|
164
|
+
</TableHead>
|
|
165
|
+
</TableRow>
|
|
166
|
+
</TableHeader>
|
|
167
|
+
</Table>,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
screen.getByRole("columnheader", { name: "Created" }).focus();
|
|
171
|
+
await user.keyboard("{Enter}");
|
|
172
|
+
expect(handleSortChange).toHaveBeenCalledWith({
|
|
173
|
+
sortKey: "createdAt",
|
|
174
|
+
sortDirection: "ascending",
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should render scoped body cells as row headers", () => {
|
|
179
|
+
renderBasicTable();
|
|
180
|
+
expect(screen.getByRole("rowheader", { name: "Zentauri" })).toHaveAttribute(
|
|
181
|
+
"data-slot",
|
|
182
|
+
"table-cell",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should not leak rowAnimation onto the rendered row", () => {
|
|
187
|
+
const { container } = render(
|
|
188
|
+
<Table>
|
|
189
|
+
<TableBody>
|
|
190
|
+
<TableRow rowAnimation="hover">
|
|
191
|
+
<TableCell>Animated elsewhere</TableCell>
|
|
192
|
+
</TableRow>
|
|
193
|
+
</TableBody>
|
|
194
|
+
</Table>,
|
|
195
|
+
);
|
|
196
|
+
expect(
|
|
197
|
+
container.querySelector('[data-slot="table-row"]'),
|
|
198
|
+
).not.toHaveAttribute("rowAnimation");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should forward refs to the table element", () => {
|
|
202
|
+
const ref = createRef<HTMLTableElement>();
|
|
203
|
+
render(<Table ref={ref} />);
|
|
204
|
+
expect(ref.current).toBeInstanceOf(HTMLTableElement);
|
|
205
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("table");
|
|
206
|
+
});
|
|
207
|
+
});
|
package/src/ui/table/types.ts
CHANGED
|
@@ -11,6 +11,16 @@ import type {
|
|
|
11
11
|
import type { tableVariants } from "./variants";
|
|
12
12
|
|
|
13
13
|
export type TableAnimation = "none" | "hover";
|
|
14
|
+
export type TableSortDirection = "ascending" | "descending" | "none";
|
|
15
|
+
|
|
16
|
+
export type TableSortState<TKey extends string = string> = {
|
|
17
|
+
sortKey?: TKey;
|
|
18
|
+
sortDirection: TableSortDirection;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TableSortChangeHandler<TKey extends string = string> = (
|
|
22
|
+
nextSort: TableSortState<TKey>,
|
|
23
|
+
) => void;
|
|
14
24
|
|
|
15
25
|
type TableVariantProps = VariantProps<typeof tableVariants>;
|
|
16
26
|
|
|
@@ -31,7 +41,9 @@ export type TableSectionProps = {
|
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
export type TableHeadCellProps = ThHTMLAttributes<HTMLTableCellElement> & {
|
|
34
|
-
|
|
44
|
+
sortKey?: string;
|
|
45
|
+
sortDirection?: TableSortDirection;
|
|
46
|
+
onSortChange?: TableSortChangeHandler;
|
|
35
47
|
ref?: Ref<HTMLTableCellElement>;
|
|
36
48
|
};
|
|
37
49
|
|