@handled-ai/design-system 0.14.8 → 0.14.10
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/dist/components/data-table-filter.d.ts +3 -1
- package/dist/components/data-table-filter.js +9 -3
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/virtualized-data-table.d.ts +6 -2
- package/dist/components/virtualized-data-table.js +51 -20
- package/dist/components/virtualized-data-table.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/virtualized-data-table-resize.test.tsx +524 -0
- package/src/components/data-table-filter.tsx +12 -2
- package/src/components/virtualized-data-table.tsx +40 -1
- package/src/index.ts +1 -0
|
@@ -17,7 +17,9 @@ interface DataTableFilterProps {
|
|
|
17
17
|
selectedFilters: Record<string, string[]>;
|
|
18
18
|
onToggleFilter: (categoryId: string, option: string) => void;
|
|
19
19
|
className?: string;
|
|
20
|
+
/** Minimum number of options before showing the sub-menu search input. Defaults to 8. */
|
|
21
|
+
optionSearchThreshold?: number;
|
|
20
22
|
}
|
|
21
|
-
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, }: DataTableFilterProps): React.JSX.Element;
|
|
23
|
+
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, optionSearchThreshold, }: DataTableFilterProps): React.JSX.Element;
|
|
22
24
|
|
|
23
25
|
export { DataTableFilter, type DataTableFilterCategory, type FilterOption };
|
|
@@ -44,7 +44,8 @@ function DataTableFilter({
|
|
|
44
44
|
categories,
|
|
45
45
|
selectedFilters,
|
|
46
46
|
onToggleFilter,
|
|
47
|
-
className
|
|
47
|
+
className,
|
|
48
|
+
optionSearchThreshold = 8
|
|
48
49
|
}) {
|
|
49
50
|
const [query, setQuery] = React.useState("");
|
|
50
51
|
const [subQueries, setSubQueries] = React.useState({});
|
|
@@ -126,7 +127,7 @@ function DataTableFilter({
|
|
|
126
127
|
category.label
|
|
127
128
|
] }),
|
|
128
129
|
/* @__PURE__ */ jsxs(DropdownMenuSubContent, { className: "max-h-[320px] w-52 overflow-y-auto p-1", children: [
|
|
129
|
-
category.options.length >
|
|
130
|
+
category.options.length > optionSearchThreshold && /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10 border-b border-border bg-popover p-1.5", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
130
131
|
/* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }),
|
|
131
132
|
/* @__PURE__ */ jsx(
|
|
132
133
|
"input",
|
|
@@ -136,7 +137,12 @@ function DataTableFilter({
|
|
|
136
137
|
value: (_b = subQueries[category.id]) != null ? _b : "",
|
|
137
138
|
onChange: (e) => setSubQueries((prev) => __spreadProps(__spreadValues({}, prev), { [category.id]: e.target.value })),
|
|
138
139
|
onClick: (e) => e.stopPropagation(),
|
|
139
|
-
onKeyDown: (e) =>
|
|
140
|
+
onKeyDown: (e) => {
|
|
141
|
+
const navKeys = ["ArrowDown", "ArrowUp", "Enter", "Escape", "Tab"];
|
|
142
|
+
if (!navKeys.includes(e.key)) {
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
140
146
|
}
|
|
141
147
|
)
|
|
142
148
|
] }) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ListFilter, Search } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n const activeCount = React.useMemo(\n () =>\n Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n ),\n [selectedFilters]\n )\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — only for categories with many options */}\n {category.options.length > 7 && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => e.stopPropagation()}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ListFilter, Search } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n const activeCount = React.useMemo(\n () =>\n Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n ),\n [selectedFilters]\n )\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — only for categories with many options */}\n {category.options.length > optionSearchThreshold && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { ColumnDef,
|
|
2
|
+
import { ColumnDef, ColumnSizingState, OnChangeFn, SortingState, ColumnFiltersState, VisibilityState } from '@tanstack/react-table';
|
|
3
3
|
|
|
4
4
|
interface VirtualizedDataTableProps<TData> {
|
|
5
5
|
columns: ColumnDef<TData, any>[];
|
|
@@ -13,6 +13,10 @@ interface VirtualizedDataTableProps<TData> {
|
|
|
13
13
|
reachBottomThreshold?: number;
|
|
14
14
|
hasMore?: boolean;
|
|
15
15
|
isFetchingMore?: boolean;
|
|
16
|
+
enableColumnResizing?: boolean;
|
|
17
|
+
columnResizeMode?: "onChange" | "onEnd";
|
|
18
|
+
columnSizing?: ColumnSizingState;
|
|
19
|
+
onColumnSizingChange?: OnChangeFn<ColumnSizingState>;
|
|
16
20
|
sorting?: SortingState;
|
|
17
21
|
onSortingChange?: OnChangeFn<SortingState>;
|
|
18
22
|
columnFilters?: ColumnFiltersState;
|
|
@@ -25,6 +29,6 @@ interface VirtualizedDataTableProps<TData> {
|
|
|
25
29
|
emptyDescription?: string;
|
|
26
30
|
className?: string;
|
|
27
31
|
}
|
|
28
|
-
declare function VirtualizedDataTable<TData>({ columns, data, height, estimateRowHeight, overscan, onRowClick, getRowId, onReachBottom, reachBottomThreshold, hasMore, isFetchingMore, sorting, onSortingChange, columnFilters, onColumnFiltersChange, columnVisibility, onColumnVisibilityChange, isLoading, emptyIcon, emptyMessage, emptyDescription, className, }: VirtualizedDataTableProps<TData>): React.JSX.Element;
|
|
32
|
+
declare function VirtualizedDataTable<TData>({ columns, data, height, estimateRowHeight, overscan, onRowClick, getRowId, onReachBottom, reachBottomThreshold, hasMore, isFetchingMore, enableColumnResizing, columnResizeMode, columnSizing, onColumnSizingChange, sorting, onSortingChange, columnFilters, onColumnFiltersChange, columnVisibility, onColumnVisibilityChange, isLoading, emptyIcon, emptyMessage, emptyDescription, className, }: VirtualizedDataTableProps<TData>): React.JSX.Element;
|
|
29
33
|
|
|
30
34
|
export { VirtualizedDataTable, type VirtualizedDataTableProps };
|
|
@@ -42,6 +42,10 @@ function VirtualizedDataTable({
|
|
|
42
42
|
reachBottomThreshold = 5,
|
|
43
43
|
hasMore = true,
|
|
44
44
|
isFetchingMore,
|
|
45
|
+
enableColumnResizing = false,
|
|
46
|
+
columnResizeMode = "onEnd",
|
|
47
|
+
columnSizing,
|
|
48
|
+
onColumnSizingChange,
|
|
45
49
|
sorting,
|
|
46
50
|
onSortingChange,
|
|
47
51
|
columnFilters,
|
|
@@ -63,6 +67,9 @@ function VirtualizedDataTable({
|
|
|
63
67
|
const [internalColumnVisibility, setInternalColumnVisibility] = React.useState({});
|
|
64
68
|
const resolvedColumnVisibility = columnVisibility != null ? columnVisibility : internalColumnVisibility;
|
|
65
69
|
const resolvedOnColumnVisibilityChange = onColumnVisibilityChange != null ? onColumnVisibilityChange : setInternalColumnVisibility;
|
|
70
|
+
const [internalColumnSizing, setInternalColumnSizing] = React.useState({});
|
|
71
|
+
const resolvedColumnSizing = columnSizing != null ? columnSizing : internalColumnSizing;
|
|
72
|
+
const resolvedOnColumnSizingChange = onColumnSizingChange != null ? onColumnSizingChange : setInternalColumnSizing;
|
|
66
73
|
const table = useReactTable(__spreadProps(__spreadValues({
|
|
67
74
|
data,
|
|
68
75
|
columns
|
|
@@ -70,11 +77,15 @@ function VirtualizedDataTable({
|
|
|
70
77
|
state: {
|
|
71
78
|
sorting: resolvedSorting,
|
|
72
79
|
columnFilters: resolvedColumnFilters,
|
|
73
|
-
columnVisibility: resolvedColumnVisibility
|
|
80
|
+
columnVisibility: resolvedColumnVisibility,
|
|
81
|
+
columnSizing: resolvedColumnSizing
|
|
74
82
|
},
|
|
75
83
|
onSortingChange: resolvedOnSortingChange,
|
|
76
84
|
onColumnFiltersChange: resolvedOnColumnFiltersChange,
|
|
77
85
|
onColumnVisibilityChange: resolvedOnColumnVisibilityChange,
|
|
86
|
+
onColumnSizingChange: resolvedOnColumnSizingChange,
|
|
87
|
+
enableColumnResizing,
|
|
88
|
+
columnResizeMode,
|
|
78
89
|
manualSorting: true,
|
|
79
90
|
manualFiltering: true,
|
|
80
91
|
manualPagination: true,
|
|
@@ -130,10 +141,13 @@ function VirtualizedDataTable({
|
|
|
130
141
|
{
|
|
131
142
|
className: "flex w-max min-w-full border-b border-border/50",
|
|
132
143
|
role: "row",
|
|
133
|
-
children: headerGroup.headers.map((header, colIdx) => /* @__PURE__ */
|
|
144
|
+
children: headerGroup.headers.map((header, colIdx) => /* @__PURE__ */ jsxs(
|
|
134
145
|
"div",
|
|
135
146
|
{
|
|
136
|
-
className:
|
|
147
|
+
className: cn(
|
|
148
|
+
"h-9 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative",
|
|
149
|
+
header.column.getCanResize() && "pr-4"
|
|
150
|
+
),
|
|
137
151
|
style: {
|
|
138
152
|
width: header.getSize(),
|
|
139
153
|
minWidth: header.getSize()
|
|
@@ -141,24 +155,41 @@ function VirtualizedDataTable({
|
|
|
141
155
|
role: "columnheader",
|
|
142
156
|
"aria-colindex": colIdx + 1,
|
|
143
157
|
"aria-sort": header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : header.column.getCanSort() ? "none" : void 0,
|
|
144
|
-
children:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
children: [
|
|
159
|
+
header.isPlaceholder ? null : header.column.getCanSort() ? /* @__PURE__ */ jsxs(
|
|
160
|
+
"button",
|
|
161
|
+
{
|
|
162
|
+
type: "button",
|
|
163
|
+
className: "inline-flex items-center gap-1 hover:text-foreground transition-colors",
|
|
164
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
165
|
+
children: [
|
|
166
|
+
flexRender(
|
|
167
|
+
header.column.columnDef.header,
|
|
168
|
+
header.getContext()
|
|
169
|
+
),
|
|
170
|
+
header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "w-3 h-3" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { className: "w-3 h-3" }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "w-3 h-3 opacity-40" })
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
) : flexRender(
|
|
174
|
+
header.column.columnDef.header,
|
|
175
|
+
header.getContext()
|
|
176
|
+
),
|
|
177
|
+
header.column.getCanResize() && /* @__PURE__ */ jsx(
|
|
178
|
+
"div",
|
|
179
|
+
{
|
|
180
|
+
onMouseDown: header.getResizeHandler(),
|
|
181
|
+
onTouchStart: header.getResizeHandler(),
|
|
182
|
+
className: cn(
|
|
183
|
+
"absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
|
|
184
|
+
"after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
|
|
185
|
+
"after:bg-transparent hover:after:bg-primary/30",
|
|
186
|
+
header.column.getIsResizing() && "after:bg-primary/50"
|
|
154
187
|
),
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
header.getContext()
|
|
161
|
-
)
|
|
188
|
+
role: "separator",
|
|
189
|
+
"aria-orientation": "vertical"
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
]
|
|
162
193
|
},
|
|
163
194
|
header.id
|
|
164
195
|
))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/virtualized-data-table.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport {\n useReactTable,\n getCoreRowModel,\n flexRender,\n type ColumnDef,\n type SortingState,\n type ColumnFiltersState,\n type VisibilityState,\n type OnChangeFn,\n} from \"@tanstack/react-table\"\nimport { ArrowDown, ArrowUp, ArrowUpDown, SearchX, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\n\nexport interface VirtualizedDataTableProps<TData> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n columns: ColumnDef<TData, any>[]\n data: TData[]\n\n // Virtualization\n height?: number | string\n estimateRowHeight?: number\n overscan?: number\n\n // Row interaction\n onRowClick?: (row: TData) => void\n getRowId?: (original: TData, index: number) => string\n\n // Infinite scroll\n onReachBottom?: () => void\n reachBottomThreshold?: number\n hasMore?: boolean\n isFetchingMore?: boolean\n\n // Server-driven state (controlled) — omit for internal state\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n columnFilters?: ColumnFiltersState\n onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>\n columnVisibility?: VisibilityState\n onColumnVisibilityChange?: OnChangeFn<VisibilityState>\n\n // Loading / Empty state\n isLoading?: boolean\n emptyIcon?: React.ReactNode\n emptyMessage?: string\n emptyDescription?: string\n\n // Styling\n className?: string\n}\n\nexport function VirtualizedDataTable<TData>({\n columns,\n data,\n height = 600,\n estimateRowHeight = 48,\n overscan = 8,\n onRowClick,\n getRowId,\n onReachBottom,\n reachBottomThreshold = 5,\n hasMore = true,\n isFetchingMore,\n sorting,\n onSortingChange,\n columnFilters,\n onColumnFiltersChange,\n columnVisibility,\n onColumnVisibilityChange,\n isLoading,\n emptyIcon,\n emptyMessage = \"No rows found\",\n emptyDescription = \"Try adjusting your filters\",\n className,\n}: VirtualizedDataTableProps<TData>) {\n // Controlled/uncontrolled state for sorting\n const [internalSorting, setInternalSorting] = React.useState<SortingState>([])\n const resolvedSorting = sorting ?? internalSorting\n const resolvedOnSortingChange = onSortingChange ?? setInternalSorting\n\n // Controlled/uncontrolled state for column filters\n const [internalColumnFilters, setInternalColumnFilters] =\n React.useState<ColumnFiltersState>([])\n const resolvedColumnFilters = columnFilters ?? internalColumnFilters\n const resolvedOnColumnFiltersChange =\n onColumnFiltersChange ?? setInternalColumnFilters\n\n // Controlled/uncontrolled state for column visibility\n const [internalColumnVisibility, setInternalColumnVisibility] =\n React.useState<VisibilityState>({})\n const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility\n const resolvedOnColumnVisibilityChange =\n onColumnVisibilityChange ?? setInternalColumnVisibility\n\n // TanStack Table setup\n const table = useReactTable({\n data,\n columns,\n ...(getRowId ? { getRowId } : {}),\n state: {\n sorting: resolvedSorting,\n columnFilters: resolvedColumnFilters,\n columnVisibility: resolvedColumnVisibility,\n },\n onSortingChange: resolvedOnSortingChange,\n onColumnFiltersChange: resolvedOnColumnFiltersChange,\n onColumnVisibilityChange: resolvedOnColumnVisibilityChange,\n manualSorting: true,\n manualFiltering: true,\n manualPagination: true,\n getCoreRowModel: getCoreRowModel(),\n })\n\n // Virtualizer setup\n const scrollContainerRef = React.useRef<HTMLDivElement>(null)\n const rows = table.getRowModel().rows\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scrollContainerRef.current,\n estimateSize: () => estimateRowHeight,\n overscan,\n measureElement: (element) => element.getBoundingClientRect().height,\n })\n\n // Infinite scroll detection\n const lastTriggeredDataLengthRef = React.useRef<number>(0)\n\n // Derive a stable primitive for the last visible virtual-item index so the\n // effect below doesn't re-run on every render (getVirtualItems() returns a\n // new array reference each call).\n const virtualItems = virtualizer.getVirtualItems()\n const lastVirtualItemIndex =\n virtualItems.length > 0\n ? virtualItems[virtualItems.length - 1].index\n : -1\n\n React.useEffect(() => {\n if (!onReachBottom || isFetchingMore || hasMore === false) return\n if (lastVirtualItemIndex < 0) return\n if (lastVirtualItemIndex < rows.length - reachBottomThreshold) return\n\n // Prevent re-firing until data.length changes (i.e. new page loaded).\n if (lastTriggeredDataLengthRef.current === data.length) return\n lastTriggeredDataLengthRef.current = data.length\n\n onReachBottom()\n }, [\n lastVirtualItemIndex,\n rows.length,\n data.length,\n onReachBottom,\n isFetchingMore,\n hasMore,\n reachBottomThreshold,\n ])\n\n return (\n <div className={cn(\n \"w-full\",\n typeof height === \"string\" && height.trim().endsWith(\"%\") && \"h-full\",\n className,\n )}>\n <div\n ref={scrollContainerRef}\n className=\"relative overflow-auto\"\n style={{\n height: typeof height === \"number\" ? `${height}px` : height,\n contain: \"strict\",\n }}\n role=\"table\"\n aria-rowcount={data.length}\n aria-colcount={table.getVisibleLeafColumns().length}\n >\n {/* Sticky header */}\n <div className=\"sticky top-0 z-10 bg-background\" role=\"rowgroup\">\n {table.getHeaderGroups().map((headerGroup) => (\n <div\n key={headerGroup.id}\n className=\"flex w-max min-w-full border-b border-border/50\"\n role=\"row\"\n >\n {headerGroup.headers.map((header, colIdx) => (\n <div\n key={header.id}\n className=\"h-9 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap\"\n style={{\n width: header.getSize(),\n minWidth: header.getSize(),\n }}\n role=\"columnheader\"\n aria-colindex={colIdx + 1}\n aria-sort={\n header.column.getIsSorted() === \"asc\"\n ? \"ascending\"\n : header.column.getIsSorted() === \"desc\"\n ? \"descending\"\n : header.column.getCanSort()\n ? \"none\"\n : undefined\n }\n >\n {header.isPlaceholder ? null : header.column.getCanSort() ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-foreground transition-colors\"\n onClick={header.column.getToggleSortingHandler()}\n >\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n {header.column.getIsSorted() === \"asc\" ? (\n <ArrowUp className=\"w-3 h-3\" />\n ) : header.column.getIsSorted() === \"desc\" ? (\n <ArrowDown className=\"w-3 h-3\" />\n ) : (\n <ArrowUpDown className=\"w-3 h-3 opacity-40\" />\n )}\n </button>\n ) : (\n flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )\n )}\n </div>\n ))}\n </div>\n ))}\n </div>\n\n {/* Virtualized body or empty state */}\n {rows.length > 0 ? (\n <div\n role=\"rowgroup\"\n style={{\n height: virtualizer.getTotalSize(),\n width: \"100%\",\n position: \"relative\",\n }}\n >\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const row = rows[virtualRow.index]\n return (\n <div\n key={row.id}\n data-index={virtualRow.index}\n ref={virtualizer.measureElement}\n className={cn(\n \"absolute left-0 w-max min-w-full flex group transition-colors\",\n onRowClick && \"cursor-pointer\",\n )}\n style={{\n transform: `translateY(${virtualRow.start}px)`,\n }}\n role=\"row\"\n aria-rowindex={virtualRow.index + 2}\n onClick={() => onRowClick?.(row.original)}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick(row.original)\n }\n }\n : undefined\n }\n >\n {row.getVisibleCells().map((cell, colIdx) => (\n <div\n key={cell.id}\n className=\"px-3 py-3 flex items-center whitespace-nowrap group-hover:bg-muted/50\"\n style={{\n width: cell.column.getSize(),\n minWidth: cell.column.getSize(),\n }}\n role=\"cell\"\n aria-colindex={colIdx + 1}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </div>\n ))}\n </div>\n )\n })}\n </div>\n ) : isLoading ? (\n <div className=\"flex flex-col items-center justify-center gap-2 text-muted-foreground py-20\">\n <Loader2 className=\"h-7 w-7 animate-spin opacity-60\" />\n <p className=\"text-sm font-medium\">Loading...</p>\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center gap-1 text-muted-foreground py-20\">\n {emptyIcon ?? <SearchX className=\"h-7 w-7 opacity-40\" />}\n <p className=\"text-sm font-medium\">{emptyMessage}</p>\n <p className=\"text-xs\">{emptyDescription}</p>\n </div>\n )}\n\n {/* Loading indicator */}\n {isFetchingMore && (\n <div className=\"flex items-center justify-center py-4\">\n <Loader2 className=\"h-5 w-5 animate-spin text-muted-foreground\" />\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/virtualized-data-table.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport {\n useReactTable,\n getCoreRowModel,\n flexRender,\n type ColumnDef,\n type SortingState,\n type ColumnFiltersState,\n type VisibilityState,\n type ColumnSizingState,\n type OnChangeFn,\n} from \"@tanstack/react-table\"\nimport { ArrowDown, ArrowUp, ArrowUpDown, SearchX, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\n\nexport interface VirtualizedDataTableProps<TData> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n columns: ColumnDef<TData, any>[]\n data: TData[]\n\n // Virtualization\n height?: number | string\n estimateRowHeight?: number\n overscan?: number\n\n // Row interaction\n onRowClick?: (row: TData) => void\n getRowId?: (original: TData, index: number) => string\n\n // Infinite scroll\n onReachBottom?: () => void\n reachBottomThreshold?: number\n hasMore?: boolean\n isFetchingMore?: boolean\n\n // Column resizing\n enableColumnResizing?: boolean\n columnResizeMode?: \"onChange\" | \"onEnd\"\n columnSizing?: ColumnSizingState\n onColumnSizingChange?: OnChangeFn<ColumnSizingState>\n\n // Server-driven state (controlled) — omit for internal state\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n columnFilters?: ColumnFiltersState\n onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>\n columnVisibility?: VisibilityState\n onColumnVisibilityChange?: OnChangeFn<VisibilityState>\n\n // Loading / Empty state\n isLoading?: boolean\n emptyIcon?: React.ReactNode\n emptyMessage?: string\n emptyDescription?: string\n\n // Styling\n className?: string\n}\n\nexport function VirtualizedDataTable<TData>({\n columns,\n data,\n height = 600,\n estimateRowHeight = 48,\n overscan = 8,\n onRowClick,\n getRowId,\n onReachBottom,\n reachBottomThreshold = 5,\n hasMore = true,\n isFetchingMore,\n enableColumnResizing = false,\n columnResizeMode = \"onEnd\",\n columnSizing,\n onColumnSizingChange,\n sorting,\n onSortingChange,\n columnFilters,\n onColumnFiltersChange,\n columnVisibility,\n onColumnVisibilityChange,\n isLoading,\n emptyIcon,\n emptyMessage = \"No rows found\",\n emptyDescription = \"Try adjusting your filters\",\n className,\n}: VirtualizedDataTableProps<TData>) {\n // Controlled/uncontrolled state for sorting\n const [internalSorting, setInternalSorting] = React.useState<SortingState>([])\n const resolvedSorting = sorting ?? internalSorting\n const resolvedOnSortingChange = onSortingChange ?? setInternalSorting\n\n // Controlled/uncontrolled state for column filters\n const [internalColumnFilters, setInternalColumnFilters] =\n React.useState<ColumnFiltersState>([])\n const resolvedColumnFilters = columnFilters ?? internalColumnFilters\n const resolvedOnColumnFiltersChange =\n onColumnFiltersChange ?? setInternalColumnFilters\n\n // Controlled/uncontrolled state for column visibility\n const [internalColumnVisibility, setInternalColumnVisibility] =\n React.useState<VisibilityState>({})\n const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility\n const resolvedOnColumnVisibilityChange =\n onColumnVisibilityChange ?? setInternalColumnVisibility\n\n // Controlled/uncontrolled state for column sizing\n const [internalColumnSizing, setInternalColumnSizing] =\n React.useState<ColumnSizingState>({})\n const resolvedColumnSizing = columnSizing ?? internalColumnSizing\n const resolvedOnColumnSizingChange =\n onColumnSizingChange ?? setInternalColumnSizing\n\n // TanStack Table setup\n const table = useReactTable({\n data,\n columns,\n ...(getRowId ? { getRowId } : {}),\n state: {\n sorting: resolvedSorting,\n columnFilters: resolvedColumnFilters,\n columnVisibility: resolvedColumnVisibility,\n columnSizing: resolvedColumnSizing,\n },\n onSortingChange: resolvedOnSortingChange,\n onColumnFiltersChange: resolvedOnColumnFiltersChange,\n onColumnVisibilityChange: resolvedOnColumnVisibilityChange,\n onColumnSizingChange: resolvedOnColumnSizingChange,\n enableColumnResizing,\n columnResizeMode,\n manualSorting: true,\n manualFiltering: true,\n manualPagination: true,\n getCoreRowModel: getCoreRowModel(),\n })\n\n // Virtualizer setup\n const scrollContainerRef = React.useRef<HTMLDivElement>(null)\n const rows = table.getRowModel().rows\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scrollContainerRef.current,\n estimateSize: () => estimateRowHeight,\n overscan,\n measureElement: (element) => element.getBoundingClientRect().height,\n })\n\n // Infinite scroll detection\n const lastTriggeredDataLengthRef = React.useRef<number>(0)\n\n // Derive a stable primitive for the last visible virtual-item index so the\n // effect below doesn't re-run on every render (getVirtualItems() returns a\n // new array reference each call).\n const virtualItems = virtualizer.getVirtualItems()\n const lastVirtualItemIndex =\n virtualItems.length > 0\n ? virtualItems[virtualItems.length - 1].index\n : -1\n\n React.useEffect(() => {\n if (!onReachBottom || isFetchingMore || hasMore === false) return\n if (lastVirtualItemIndex < 0) return\n if (lastVirtualItemIndex < rows.length - reachBottomThreshold) return\n\n // Prevent re-firing until data.length changes (i.e. new page loaded).\n if (lastTriggeredDataLengthRef.current === data.length) return\n lastTriggeredDataLengthRef.current = data.length\n\n onReachBottom()\n }, [\n lastVirtualItemIndex,\n rows.length,\n data.length,\n onReachBottom,\n isFetchingMore,\n hasMore,\n reachBottomThreshold,\n ])\n\n return (\n <div className={cn(\n \"w-full\",\n typeof height === \"string\" && height.trim().endsWith(\"%\") && \"h-full\",\n className,\n )}>\n <div\n ref={scrollContainerRef}\n className=\"relative overflow-auto\"\n style={{\n height: typeof height === \"number\" ? `${height}px` : height,\n contain: \"strict\",\n }}\n role=\"table\"\n aria-rowcount={data.length}\n aria-colcount={table.getVisibleLeafColumns().length}\n >\n {/* Sticky header */}\n <div className=\"sticky top-0 z-10 bg-background\" role=\"rowgroup\">\n {table.getHeaderGroups().map((headerGroup) => (\n <div\n key={headerGroup.id}\n className=\"flex w-max min-w-full border-b border-border/50\"\n role=\"row\"\n >\n {headerGroup.headers.map((header, colIdx) => (\n <div\n key={header.id}\n className={cn(\n \"h-9 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative\",\n header.column.getCanResize() && \"pr-4\",\n )}\n style={{\n width: header.getSize(),\n minWidth: header.getSize(),\n }}\n role=\"columnheader\"\n aria-colindex={colIdx + 1}\n aria-sort={\n header.column.getIsSorted() === \"asc\"\n ? \"ascending\"\n : header.column.getIsSorted() === \"desc\"\n ? \"descending\"\n : header.column.getCanSort()\n ? \"none\"\n : undefined\n }\n >\n {header.isPlaceholder ? null : header.column.getCanSort() ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-foreground transition-colors\"\n onClick={header.column.getToggleSortingHandler()}\n >\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n {header.column.getIsSorted() === \"asc\" ? (\n <ArrowUp className=\"w-3 h-3\" />\n ) : header.column.getIsSorted() === \"desc\" ? (\n <ArrowDown className=\"w-3 h-3\" />\n ) : (\n <ArrowUpDown className=\"w-3 h-3 opacity-40\" />\n )}\n </button>\n ) : (\n flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )\n )}\n {header.column.getCanResize() && (\n <div\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n className={cn(\n \"absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none\",\n \"after:absolute after:right-1.5 after:top-0 after:h-full after:w-px\",\n \"after:bg-transparent hover:after:bg-primary/30\",\n header.column.getIsResizing() && \"after:bg-primary/50\",\n )}\n role=\"separator\"\n aria-orientation=\"vertical\"\n />\n )}\n </div>\n ))}\n </div>\n ))}\n </div>\n\n {/* Virtualized body or empty state */}\n {rows.length > 0 ? (\n <div\n role=\"rowgroup\"\n style={{\n height: virtualizer.getTotalSize(),\n width: \"100%\",\n position: \"relative\",\n }}\n >\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const row = rows[virtualRow.index]\n return (\n <div\n key={row.id}\n data-index={virtualRow.index}\n ref={virtualizer.measureElement}\n className={cn(\n \"absolute left-0 w-max min-w-full flex group transition-colors\",\n onRowClick && \"cursor-pointer\",\n )}\n style={{\n transform: `translateY(${virtualRow.start}px)`,\n }}\n role=\"row\"\n aria-rowindex={virtualRow.index + 2}\n onClick={() => onRowClick?.(row.original)}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick(row.original)\n }\n }\n : undefined\n }\n >\n {row.getVisibleCells().map((cell, colIdx) => (\n <div\n key={cell.id}\n className=\"px-3 py-3 flex items-center whitespace-nowrap group-hover:bg-muted/50\"\n style={{\n width: cell.column.getSize(),\n minWidth: cell.column.getSize(),\n }}\n role=\"cell\"\n aria-colindex={colIdx + 1}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </div>\n ))}\n </div>\n )\n })}\n </div>\n ) : isLoading ? (\n <div className=\"flex flex-col items-center justify-center gap-2 text-muted-foreground py-20\">\n <Loader2 className=\"h-7 w-7 animate-spin opacity-60\" />\n <p className=\"text-sm font-medium\">Loading...</p>\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center gap-1 text-muted-foreground py-20\">\n {emptyIcon ?? <SearchX className=\"h-7 w-7 opacity-40\" />}\n <p className=\"text-sm font-medium\">{emptyMessage}</p>\n <p className=\"text-xs\">{emptyDescription}</p>\n </div>\n )}\n\n {/* Loading indicator */}\n {isFetchingMore && (\n <div className=\"flex items-center justify-center py-4\">\n <Loader2 className=\"h-5 w-5 animate-spin text-muted-foreground\" />\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './comp
|
|
|
74
74
|
export { VariableAutocomplete, VariableAutocompleteProps, VariableDef, VariableGroup } from './components/variable-autocomplete.js';
|
|
75
75
|
export { ViewMode, ViewModeToggle, ViewModeToggleProps } from './components/view-mode-toggle.js';
|
|
76
76
|
export { VirtualizedDataTable, VirtualizedDataTableProps } from './components/virtualized-data-table.js';
|
|
77
|
+
export { ColumnSizingState } from '@tanstack/react-table';
|
|
77
78
|
export { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent } from './charts/chart.js';
|
|
78
79
|
export { CHART_CURSOR_STYLE, CHART_TOOLTIP_STYLE, ChartTooltipEntry, SimpleChartTooltip, SimpleChartTooltipProps } from './charts/chart-tooltip.js';
|
|
79
80
|
export { BarChartComponent, BarChartComponentProps, BarSeries } from './charts/bar-chart-component.js';
|
|
@@ -95,6 +96,5 @@ import 'react';
|
|
|
95
96
|
import 'radix-ui';
|
|
96
97
|
import 'class-variance-authority/types';
|
|
97
98
|
import 'class-variance-authority';
|
|
98
|
-
import '@tanstack/react-table';
|
|
99
99
|
import 'recharts';
|
|
100
100
|
import 'lucide-react';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system\n * UI components and utilities (shadcn-style, New York)\n */\n\n// Utilities\nexport { cn } from \"./lib/utils\"\nexport { BRAND_ICONS, BRAND_GRAPHICS } from \"./lib/icons\"\n\n// Hooks\nexport { useIsMobile } from \"./hooks/use-mobile\"\n\n// Components (light — no recharts/nivo/three transitive deps)\nexport * from \"./components/activity-detail\"\nexport * from \"./components/activity-log\"\nexport * from \"./components/agent-popover\"\nexport * from \"./components/agent-widget\"\nexport * from \"./components/avatar\"\nexport * from \"./components/badge\"\nexport * from \"./components/button\"\nexport * from \"./components/card\"\nexport * from \"./components/compliance-badge\"\nexport * from \"./components/contact-chip\"\nexport * from \"./components/contact-list\"\nexport * from \"./components/dashboard-cards\"\nexport * from \"./components/data-table\"\nexport * from \"./components/data-table-display\"\nexport * from \"./components/data-table-filter\"\nexport * from \"./components/data-table-quick-views\"\nexport * from \"./components/data-table-toolbar\"\nexport * from \"./components/detail-view\"\nexport * from \"./components/dialog\"\nexport * from \"./components/dropdown-menu\"\nexport * from \"./components/empty-state\"\nexport * from \"./components/entity-panel\"\nexport * from \"./components/filter-chip\"\nexport * from \"./components/inbox-row\"\nexport * from \"./components/inbox-toolbar\"\nexport * from \"./components/inline-banner\"\nexport * from \"./components/input\"\nexport * from \"./components/insights-filter-bar\"\nexport * from \"./components/item-list\"\nexport * from \"./components/item-list-display\"\nexport * from \"./components/item-list-filter\"\nexport * from \"./components/item-list-toolbar\"\nexport * from \"./components/kbd-hint\"\nexport * from \"./components/label\"\nexport * from \"./components/message\"\nexport * from \"./components/metric-card\"\nexport * from \"./components/performance-metrics-table\"\nexport * from \"./components/preview-list\"\nexport * from \"./components/progress\"\nexport * from \"./components/quick-action-chat-area\"\nexport {\n QuickActionModal,\n type QuickActionPriority,\n type QuickActionTaskDraft,\n type QuickActionTemplate,\n} from \"./components/quick-action-modal\"\nexport * from \"./components/quick-action-sidebar-nav\"\nexport * from \"./components/recommended-actions-section\"\nexport * from \"./components/report-card\"\nexport * from \"./components/rich-text-toolbar\"\nexport * from \"./components/score-analysis-modal\"\nexport * from \"./components/score-breakdown\"\nexport * from \"./components/score-feedback\"\nexport * from \"./components/score-ring\"\nexport * from \"./components/scroll-area\"\nexport * from \"./components/select\"\nexport * from \"./components/separator\"\nexport * from \"./components/sheet\"\nexport * from \"./components/sidebar\"\nexport * from \"./components/signal-feedback-inline\"\nexport * from \"./components/simple-data-table\"\nexport * from \"./components/skeleton\"\nexport * from \"./components/status-badge\"\nexport * from \"./components/step-timeline\"\nexport * from \"./components/sticky-action-bar\"\nexport * from \"./components/styled-bar-list\"\nexport { DraftFeedbackInline } from \"./components/draft-feedback-inline\"\nexport type { DraftFeedbackInlineProps } from \"./components/draft-feedback-inline\"\nexport { AccountContactsPopover, BrandIcon } from \"./components/account-contacts-popover\"\nexport type { AccountContactsPopoverProps } from \"./components/account-contacts-popover\"\nexport * from \"./components/suggested-actions\"\nexport * from \"./components/switch\"\nexport * from \"./components/table\"\nexport * from \"./components/tabs\"\nexport * from \"./components/textarea\"\nexport * from \"./components/timeline-activity\"\nexport * from \"./components/tooltip\"\nexport * from \"./components/variable-autocomplete\"\nexport * from \"./components/view-mode-toggle\"\nexport * from \"./components/virtualized-data-table\"\n\n// Charts (re-exported for backward compatibility with root imports)\nexport * from \"./charts/index\"\n\n// Prototype template system (re-exported for backward compatibility)\nexport * from \"./prototype/prototype-config\"\nexport * from \"./prototype/prototype-shell\"\nexport * from \"./prototype/prototype-inbox-view\"\nexport * from \"./prototype/prototype-insights-view\"\nexport * from \"./prototype/prototype-accounts-view\"\nexport * from \"./prototype/prototype-admin-view\"\nexport * from \"./prototype/prototype-work-queue-view\"\n"],"mappings":"AAMA,SAAS,UAAU;AACnB,SAAS,aAAa,sBAAsB;AAG5C,SAAS,mBAAmB;AAG5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,2BAA2B;AAEpC,SAAS,wBAAwB,iBAAiB;AAElD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system\n * UI components and utilities (shadcn-style, New York)\n */\n\n// Utilities\nexport { cn } from \"./lib/utils\"\nexport { BRAND_ICONS, BRAND_GRAPHICS } from \"./lib/icons\"\n\n// Hooks\nexport { useIsMobile } from \"./hooks/use-mobile\"\n\n// Components (light — no recharts/nivo/three transitive deps)\nexport * from \"./components/activity-detail\"\nexport * from \"./components/activity-log\"\nexport * from \"./components/agent-popover\"\nexport * from \"./components/agent-widget\"\nexport * from \"./components/avatar\"\nexport * from \"./components/badge\"\nexport * from \"./components/button\"\nexport * from \"./components/card\"\nexport * from \"./components/compliance-badge\"\nexport * from \"./components/contact-chip\"\nexport * from \"./components/contact-list\"\nexport * from \"./components/dashboard-cards\"\nexport * from \"./components/data-table\"\nexport * from \"./components/data-table-display\"\nexport * from \"./components/data-table-filter\"\nexport * from \"./components/data-table-quick-views\"\nexport * from \"./components/data-table-toolbar\"\nexport * from \"./components/detail-view\"\nexport * from \"./components/dialog\"\nexport * from \"./components/dropdown-menu\"\nexport * from \"./components/empty-state\"\nexport * from \"./components/entity-panel\"\nexport * from \"./components/filter-chip\"\nexport * from \"./components/inbox-row\"\nexport * from \"./components/inbox-toolbar\"\nexport * from \"./components/inline-banner\"\nexport * from \"./components/input\"\nexport * from \"./components/insights-filter-bar\"\nexport * from \"./components/item-list\"\nexport * from \"./components/item-list-display\"\nexport * from \"./components/item-list-filter\"\nexport * from \"./components/item-list-toolbar\"\nexport * from \"./components/kbd-hint\"\nexport * from \"./components/label\"\nexport * from \"./components/message\"\nexport * from \"./components/metric-card\"\nexport * from \"./components/performance-metrics-table\"\nexport * from \"./components/preview-list\"\nexport * from \"./components/progress\"\nexport * from \"./components/quick-action-chat-area\"\nexport {\n QuickActionModal,\n type QuickActionPriority,\n type QuickActionTaskDraft,\n type QuickActionTemplate,\n} from \"./components/quick-action-modal\"\nexport * from \"./components/quick-action-sidebar-nav\"\nexport * from \"./components/recommended-actions-section\"\nexport * from \"./components/report-card\"\nexport * from \"./components/rich-text-toolbar\"\nexport * from \"./components/score-analysis-modal\"\nexport * from \"./components/score-breakdown\"\nexport * from \"./components/score-feedback\"\nexport * from \"./components/score-ring\"\nexport * from \"./components/scroll-area\"\nexport * from \"./components/select\"\nexport * from \"./components/separator\"\nexport * from \"./components/sheet\"\nexport * from \"./components/sidebar\"\nexport * from \"./components/signal-feedback-inline\"\nexport * from \"./components/simple-data-table\"\nexport * from \"./components/skeleton\"\nexport * from \"./components/status-badge\"\nexport * from \"./components/step-timeline\"\nexport * from \"./components/sticky-action-bar\"\nexport * from \"./components/styled-bar-list\"\nexport { DraftFeedbackInline } from \"./components/draft-feedback-inline\"\nexport type { DraftFeedbackInlineProps } from \"./components/draft-feedback-inline\"\nexport { AccountContactsPopover, BrandIcon } from \"./components/account-contacts-popover\"\nexport type { AccountContactsPopoverProps } from \"./components/account-contacts-popover\"\nexport * from \"./components/suggested-actions\"\nexport * from \"./components/switch\"\nexport * from \"./components/table\"\nexport * from \"./components/tabs\"\nexport * from \"./components/textarea\"\nexport * from \"./components/timeline-activity\"\nexport * from \"./components/tooltip\"\nexport * from \"./components/variable-autocomplete\"\nexport * from \"./components/view-mode-toggle\"\nexport * from \"./components/virtualized-data-table\"\nexport type { ColumnSizingState } from \"@tanstack/react-table\"\n\n// Charts (re-exported for backward compatibility with root imports)\nexport * from \"./charts/index\"\n\n// Prototype template system (re-exported for backward compatibility)\nexport * from \"./prototype/prototype-config\"\nexport * from \"./prototype/prototype-shell\"\nexport * from \"./prototype/prototype-inbox-view\"\nexport * from \"./prototype/prototype-insights-view\"\nexport * from \"./prototype/prototype-accounts-view\"\nexport * from \"./prototype/prototype-admin-view\"\nexport * from \"./prototype/prototype-work-queue-view\"\n"],"mappings":"AAMA,SAAS,UAAU;AACnB,SAAS,aAAa,sBAAsB;AAG5C,SAAS,mBAAmB;AAG5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,2BAA2B;AAEpC,SAAS,wBAAwB,iBAAiB;AAElD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, fireEvent } from "@testing-library/react";
|
|
4
|
+
import { VirtualizedDataTable } from "../virtualized-data-table";
|
|
5
|
+
// Verify the barrel re-export compiles (type-only import used below in tests)
|
|
6
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
7
|
+
import type { ColumnSizingState } from "../../index";
|
|
8
|
+
|
|
9
|
+
type TestRow = { id: string; name: string; value: number };
|
|
10
|
+
|
|
11
|
+
const testColumns: ColumnDef<TestRow, unknown>[] = [
|
|
12
|
+
{ accessorKey: "name", header: "Name", size: 200, minSize: 100 },
|
|
13
|
+
{ accessorKey: "value", header: "Value", size: 150, minSize: 80 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const testData: TestRow[] = [
|
|
17
|
+
{ id: "1", name: "Alpha", value: 10 },
|
|
18
|
+
{ id: "2", name: "Beta", value: 20 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// ─── Group 1: Feature disabled by default ─────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe("VirtualizedDataTable — resize disabled by default", () => {
|
|
24
|
+
it("does not render resize handles when enableColumnResizing is omitted", () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<VirtualizedDataTable columns={testColumns} data={testData} height={300} />,
|
|
27
|
+
);
|
|
28
|
+
expect(container.querySelectorAll('[role="separator"]').length).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not render resize handles when enableColumnResizing={false} explicitly", () => {
|
|
32
|
+
const { container } = render(
|
|
33
|
+
<VirtualizedDataTable
|
|
34
|
+
columns={testColumns}
|
|
35
|
+
data={testData}
|
|
36
|
+
height={300}
|
|
37
|
+
enableColumnResizing={false}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
expect(container.querySelectorAll('[role="separator"]').length).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── Group 2: Handles appear and are correctly structured ─────────────────────
|
|
45
|
+
|
|
46
|
+
describe("VirtualizedDataTable — resize handles render when enabled", () => {
|
|
47
|
+
it("renders one resize handle per resizable column", () => {
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<VirtualizedDataTable
|
|
50
|
+
columns={testColumns}
|
|
51
|
+
data={testData}
|
|
52
|
+
height={300}
|
|
53
|
+
enableColumnResizing
|
|
54
|
+
/>,
|
|
55
|
+
);
|
|
56
|
+
expect(container.querySelectorAll('[role="separator"]').length).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("each handle has aria-orientation='vertical'", () => {
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<VirtualizedDataTable
|
|
62
|
+
columns={testColumns}
|
|
63
|
+
data={testData}
|
|
64
|
+
height={300}
|
|
65
|
+
enableColumnResizing
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
container.querySelectorAll('[role="separator"]').forEach((sep) => {
|
|
69
|
+
expect(sep.getAttribute("aria-orientation")).toBe("vertical");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("each handle has cursor-col-resize class", () => {
|
|
74
|
+
const { container } = render(
|
|
75
|
+
<VirtualizedDataTable
|
|
76
|
+
columns={testColumns}
|
|
77
|
+
data={testData}
|
|
78
|
+
height={300}
|
|
79
|
+
enableColumnResizing
|
|
80
|
+
/>,
|
|
81
|
+
);
|
|
82
|
+
container.querySelectorAll('[role="separator"]').forEach((sep) => {
|
|
83
|
+
expect(sep.classList.contains("cursor-col-resize")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("header cells have 'relative' class when resizing is enabled", () => {
|
|
88
|
+
const { container } = render(
|
|
89
|
+
<VirtualizedDataTable
|
|
90
|
+
columns={testColumns}
|
|
91
|
+
data={testData}
|
|
92
|
+
height={300}
|
|
93
|
+
enableColumnResizing
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
container.querySelectorAll('[role="columnheader"]').forEach((h) => {
|
|
97
|
+
expect((h as HTMLElement).classList.contains("relative")).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("resizable header cells get pr-4 padding to prevent overlap with sort text", () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<VirtualizedDataTable
|
|
104
|
+
columns={testColumns}
|
|
105
|
+
data={testData}
|
|
106
|
+
height={300}
|
|
107
|
+
enableColumnResizing
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
// Every header cell whose column can resize must have pr-4
|
|
111
|
+
container.querySelectorAll('[role="columnheader"]').forEach((h) => {
|
|
112
|
+
expect((h as HTMLElement).classList.contains("pr-4")).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("non-resizable columns do NOT get pr-4 padding", () => {
|
|
117
|
+
const columns: ColumnDef<TestRow, unknown>[] = [
|
|
118
|
+
{ accessorKey: "name", header: "Name", size: 200, enableResizing: false },
|
|
119
|
+
{ accessorKey: "value", header: "Value", size: 150 },
|
|
120
|
+
];
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<VirtualizedDataTable
|
|
123
|
+
columns={columns}
|
|
124
|
+
data={testData}
|
|
125
|
+
height={300}
|
|
126
|
+
enableColumnResizing
|
|
127
|
+
/>,
|
|
128
|
+
);
|
|
129
|
+
const headers = container.querySelectorAll('[role="columnheader"]');
|
|
130
|
+
// First column: enableResizing=false → no pr-4
|
|
131
|
+
expect((headers[0] as HTMLElement).classList.contains("pr-4")).toBe(false);
|
|
132
|
+
// Second column: resizable → has pr-4
|
|
133
|
+
expect((headers[1] as HTMLElement).classList.contains("pr-4")).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("does not render a handle for a column with enableResizing: false", () => {
|
|
137
|
+
const columns: ColumnDef<TestRow, unknown>[] = [
|
|
138
|
+
{ accessorKey: "name", header: "Name", size: 200, enableResizing: false },
|
|
139
|
+
{ accessorKey: "value", header: "Value", size: 150 },
|
|
140
|
+
];
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<VirtualizedDataTable
|
|
143
|
+
columns={columns}
|
|
144
|
+
data={testData}
|
|
145
|
+
height={300}
|
|
146
|
+
enableColumnResizing
|
|
147
|
+
/>,
|
|
148
|
+
);
|
|
149
|
+
expect(container.querySelectorAll('[role="separator"]').length).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Group 3: Initial sizing from column defs ─────────────────────────────────
|
|
154
|
+
|
|
155
|
+
describe("VirtualizedDataTable — initial column sizes", () => {
|
|
156
|
+
it("header cells render at their declared size", () => {
|
|
157
|
+
const { container } = render(
|
|
158
|
+
<VirtualizedDataTable
|
|
159
|
+
columns={testColumns}
|
|
160
|
+
data={testData}
|
|
161
|
+
height={300}
|
|
162
|
+
enableColumnResizing
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
const headers = container.querySelectorAll('[role="columnheader"]');
|
|
166
|
+
expect((headers[0] as HTMLElement).style.width).toBe("200px");
|
|
167
|
+
expect((headers[1] as HTMLElement).style.width).toBe("150px");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("column cannot be dragged below its minSize (onChange mode)", () => {
|
|
171
|
+
const { container } = render(
|
|
172
|
+
<VirtualizedDataTable
|
|
173
|
+
columns={testColumns}
|
|
174
|
+
data={testData}
|
|
175
|
+
height={300}
|
|
176
|
+
enableColumnResizing
|
|
177
|
+
columnResizeMode="onChange"
|
|
178
|
+
/>,
|
|
179
|
+
);
|
|
180
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
181
|
+
// Start at x=200, drag far left past minSize=100 → delta of -150 would give 50px
|
|
182
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
183
|
+
fireEvent.mouseMove(document, { clientX: 50 });
|
|
184
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
185
|
+
expect(parseInt(header.style.width, 10)).toBeGreaterThanOrEqual(100);
|
|
186
|
+
fireEvent.mouseUp(document);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─── Group 4: columnResizeMode="onEnd" (new default) ─────────────────────────
|
|
191
|
+
//
|
|
192
|
+
// With onEnd, sizing state is committed at mouseUp, not during mousemove.
|
|
193
|
+
// Width in the DOM should be unchanged mid-drag, then reflect the new size
|
|
194
|
+
// after mouseUp.
|
|
195
|
+
|
|
196
|
+
describe("VirtualizedDataTable — columnResizeMode onEnd (default)", () => {
|
|
197
|
+
it("header width does NOT change during mousemove (committed on mouseUp)", () => {
|
|
198
|
+
const { container } = render(
|
|
199
|
+
<VirtualizedDataTable
|
|
200
|
+
columns={testColumns}
|
|
201
|
+
data={testData}
|
|
202
|
+
height={300}
|
|
203
|
+
enableColumnResizing
|
|
204
|
+
// columnResizeMode defaults to "onEnd" — do not pass it explicitly
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
208
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
209
|
+
const widthBeforeDrag = header.style.width;
|
|
210
|
+
|
|
211
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
212
|
+
fireEvent.mouseMove(document, { clientX: 260 }); // mid-drag: should not change yet
|
|
213
|
+
expect(header.style.width).toBe(widthBeforeDrag);
|
|
214
|
+
|
|
215
|
+
fireEvent.mouseUp(document);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("header width changes after mouseUp completes the drag", () => {
|
|
219
|
+
const { container } = render(
|
|
220
|
+
<VirtualizedDataTable
|
|
221
|
+
columns={testColumns}
|
|
222
|
+
data={testData}
|
|
223
|
+
height={300}
|
|
224
|
+
enableColumnResizing
|
|
225
|
+
// "onEnd" default
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
229
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
230
|
+
const widthBefore = parseInt(header.style.width, 10);
|
|
231
|
+
|
|
232
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
233
|
+
fireEvent.mouseMove(document, { clientX: 260 }); // +60px delta
|
|
234
|
+
fireEvent.mouseUp(document); // commits the resize
|
|
235
|
+
|
|
236
|
+
expect(parseInt(header.style.width, 10)).toBeGreaterThan(widthBefore);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("onColumnSizingChange is called on mouseUp (not mousemove) in onEnd mode", () => {
|
|
240
|
+
const onSizingChange = vi.fn();
|
|
241
|
+
const { container } = render(
|
|
242
|
+
<VirtualizedDataTable
|
|
243
|
+
columns={testColumns}
|
|
244
|
+
data={testData}
|
|
245
|
+
height={300}
|
|
246
|
+
enableColumnResizing
|
|
247
|
+
columnSizing={{}}
|
|
248
|
+
onColumnSizingChange={onSizingChange}
|
|
249
|
+
// "onEnd" default
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
253
|
+
|
|
254
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
255
|
+
fireEvent.mouseMove(document, { clientX: 250 });
|
|
256
|
+
// Should NOT have been called yet mid-drag in onEnd mode
|
|
257
|
+
expect(onSizingChange).not.toHaveBeenCalled();
|
|
258
|
+
|
|
259
|
+
fireEvent.mouseUp(document);
|
|
260
|
+
// Now it should fire
|
|
261
|
+
expect(onSizingChange).toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── Group 5: Drag changes column width (onChange mode) ───────────────────────
|
|
266
|
+
|
|
267
|
+
describe("VirtualizedDataTable — drag resizing with columnResizeMode='onChange'", () => {
|
|
268
|
+
it("dragging right increases column width", () => {
|
|
269
|
+
const { container } = render(
|
|
270
|
+
<VirtualizedDataTable
|
|
271
|
+
columns={testColumns}
|
|
272
|
+
data={testData}
|
|
273
|
+
height={300}
|
|
274
|
+
enableColumnResizing
|
|
275
|
+
columnResizeMode="onChange"
|
|
276
|
+
/>,
|
|
277
|
+
);
|
|
278
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
279
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
280
|
+
const widthBefore = parseInt(header.style.width, 10);
|
|
281
|
+
|
|
282
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
283
|
+
fireEvent.mouseMove(document, { clientX: 260 }); // +60px delta
|
|
284
|
+
fireEvent.mouseUp(document);
|
|
285
|
+
|
|
286
|
+
expect(parseInt(header.style.width, 10)).toBeGreaterThan(widthBefore);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("dragging left decreases column width", () => {
|
|
290
|
+
const { container } = render(
|
|
291
|
+
<VirtualizedDataTable
|
|
292
|
+
columns={testColumns}
|
|
293
|
+
data={testData}
|
|
294
|
+
height={300}
|
|
295
|
+
enableColumnResizing
|
|
296
|
+
columnResizeMode="onChange"
|
|
297
|
+
/>,
|
|
298
|
+
);
|
|
299
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
300
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
301
|
+
const widthBefore = parseInt(header.style.width, 10);
|
|
302
|
+
|
|
303
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
304
|
+
fireEvent.mouseMove(document, { clientX: 160 }); // -40px delta
|
|
305
|
+
fireEvent.mouseUp(document);
|
|
306
|
+
|
|
307
|
+
expect(parseInt(header.style.width, 10)).toBeLessThan(widthBefore);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("dragging one column does not change the adjacent column's width", () => {
|
|
311
|
+
const { container } = render(
|
|
312
|
+
<VirtualizedDataTable
|
|
313
|
+
columns={testColumns}
|
|
314
|
+
data={testData}
|
|
315
|
+
height={300}
|
|
316
|
+
enableColumnResizing
|
|
317
|
+
columnResizeMode="onChange"
|
|
318
|
+
/>,
|
|
319
|
+
);
|
|
320
|
+
const separators = container.querySelectorAll('[role="separator"]');
|
|
321
|
+
const headers = container.querySelectorAll('[role="columnheader"]');
|
|
322
|
+
const secondWidthBefore = parseInt((headers[1] as HTMLElement).style.width, 10);
|
|
323
|
+
|
|
324
|
+
fireEvent.mouseDown(separators[0], { clientX: 200 });
|
|
325
|
+
fireEvent.mouseMove(document, { clientX: 260 });
|
|
326
|
+
fireEvent.mouseUp(document);
|
|
327
|
+
|
|
328
|
+
expect(parseInt((headers[1] as HTMLElement).style.width, 10)).toBe(secondWidthBefore);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("does not throw during resize in uncontrolled mode (no sizing props)", () => {
|
|
332
|
+
const { container } = render(
|
|
333
|
+
<VirtualizedDataTable
|
|
334
|
+
columns={testColumns}
|
|
335
|
+
data={testData}
|
|
336
|
+
height={300}
|
|
337
|
+
enableColumnResizing
|
|
338
|
+
columnResizeMode="onChange"
|
|
339
|
+
/>,
|
|
340
|
+
);
|
|
341
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
342
|
+
expect(() => {
|
|
343
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
344
|
+
fireEvent.mouseMove(document, { clientX: 250 });
|
|
345
|
+
fireEvent.mouseUp(document);
|
|
346
|
+
}).not.toThrow();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ─── Group 6: Controlled mode ─────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
describe("VirtualizedDataTable — controlled columnSizing", () => {
|
|
353
|
+
it("external columnSizing prop overrides the column def size", () => {
|
|
354
|
+
const sizing: ColumnSizingState = { name: 300 };
|
|
355
|
+
const { container } = render(
|
|
356
|
+
<VirtualizedDataTable
|
|
357
|
+
columns={testColumns}
|
|
358
|
+
data={testData}
|
|
359
|
+
height={300}
|
|
360
|
+
enableColumnResizing
|
|
361
|
+
columnSizing={sizing}
|
|
362
|
+
/>,
|
|
363
|
+
);
|
|
364
|
+
const headers = container.querySelectorAll('[role="columnheader"]');
|
|
365
|
+
expect((headers[0] as HTMLElement).style.width).toBe("300px");
|
|
366
|
+
// Column not in sizing map keeps its column-def default
|
|
367
|
+
expect((headers[1] as HTMLElement).style.width).toBe("150px");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("onColumnSizingChange is called when a resize drag occurs (onChange mode)", () => {
|
|
371
|
+
const onSizingChange = vi.fn();
|
|
372
|
+
const { container } = render(
|
|
373
|
+
<VirtualizedDataTable
|
|
374
|
+
columns={testColumns}
|
|
375
|
+
data={testData}
|
|
376
|
+
height={300}
|
|
377
|
+
enableColumnResizing
|
|
378
|
+
columnResizeMode="onChange"
|
|
379
|
+
columnSizing={{}}
|
|
380
|
+
onColumnSizingChange={onSizingChange}
|
|
381
|
+
/>,
|
|
382
|
+
);
|
|
383
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
384
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
385
|
+
fireEvent.mouseMove(document, { clientX: 250 });
|
|
386
|
+
expect(onSizingChange).toHaveBeenCalled();
|
|
387
|
+
fireEvent.mouseUp(document);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("renders correctly when columnSizing is provided without onColumnSizingChange", () => {
|
|
391
|
+
const sizing: ColumnSizingState = { name: 280 };
|
|
392
|
+
const { container } = render(
|
|
393
|
+
<VirtualizedDataTable
|
|
394
|
+
columns={testColumns}
|
|
395
|
+
data={testData}
|
|
396
|
+
height={300}
|
|
397
|
+
enableColumnResizing
|
|
398
|
+
columnSizing={sizing}
|
|
399
|
+
/>,
|
|
400
|
+
);
|
|
401
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
402
|
+
expect(header.style.width).toBe("280px");
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ─── Group 7: Cell widths track header widths ─────────────────────────────────
|
|
407
|
+
|
|
408
|
+
describe("VirtualizedDataTable — cell widths track column widths", () => {
|
|
409
|
+
it("row cells match the column width after a resize drag (onChange mode)", () => {
|
|
410
|
+
const { container } = render(
|
|
411
|
+
<VirtualizedDataTable
|
|
412
|
+
columns={testColumns}
|
|
413
|
+
data={testData}
|
|
414
|
+
height={300}
|
|
415
|
+
enableColumnResizing
|
|
416
|
+
columnResizeMode="onChange"
|
|
417
|
+
/>,
|
|
418
|
+
);
|
|
419
|
+
const separator = container.querySelector('[role="separator"]')!;
|
|
420
|
+
fireEvent.mouseDown(separator, { clientX: 200 });
|
|
421
|
+
fireEvent.mouseMove(document, { clientX: 260 });
|
|
422
|
+
fireEvent.mouseUp(document);
|
|
423
|
+
|
|
424
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
425
|
+
const newHeaderWidth = header.style.width;
|
|
426
|
+
|
|
427
|
+
// Data rows (aria-rowindex present) are rendered by the virtualizer.
|
|
428
|
+
// In happy-dom (no layout engine) the virtualizer may produce zero virtual
|
|
429
|
+
// items; if rows are present, verify their first cell tracks the header width.
|
|
430
|
+
const rows = container.querySelectorAll('[role="row"]');
|
|
431
|
+
for (let i = 1; i < rows.length; i++) {
|
|
432
|
+
const firstCell = rows[i].querySelector('[role="cell"]') as HTMLElement | null;
|
|
433
|
+
if (firstCell) {
|
|
434
|
+
expect(firstCell.style.width).toBe(newHeaderWidth);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ─── Group 8: No regressions with enableColumnResizing on ────────────────────
|
|
441
|
+
|
|
442
|
+
describe("VirtualizedDataTable — no regressions with enableColumnResizing on", () => {
|
|
443
|
+
it("onSortingChange fires when a sortable header is clicked", () => {
|
|
444
|
+
const columnsWithSort: ColumnDef<TestRow, unknown>[] = [
|
|
445
|
+
{ accessorKey: "name", header: "Name", size: 200, enableSorting: true },
|
|
446
|
+
{ accessorKey: "value", header: "Value", size: 150, enableSorting: true },
|
|
447
|
+
];
|
|
448
|
+
const onSortingChange = vi.fn();
|
|
449
|
+
const { container } = render(
|
|
450
|
+
<VirtualizedDataTable
|
|
451
|
+
columns={columnsWithSort}
|
|
452
|
+
data={testData}
|
|
453
|
+
height={300}
|
|
454
|
+
enableColumnResizing
|
|
455
|
+
sorting={[]}
|
|
456
|
+
onSortingChange={onSortingChange}
|
|
457
|
+
/>,
|
|
458
|
+
);
|
|
459
|
+
const sortButton = container.querySelector('[role="columnheader"] button')!;
|
|
460
|
+
fireEvent.click(sortButton);
|
|
461
|
+
expect(onSortingChange).toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("onRowClick prop is accepted without error; scroll container is present", () => {
|
|
465
|
+
// The virtualizer produces no virtual rows in happy-dom (no layout engine),
|
|
466
|
+
// so we verify the component mounts cleanly with onRowClick wired and that
|
|
467
|
+
// the table scroll container is in the DOM.
|
|
468
|
+
const onRowClick = vi.fn();
|
|
469
|
+
const { container } = render(
|
|
470
|
+
<VirtualizedDataTable
|
|
471
|
+
columns={testColumns}
|
|
472
|
+
data={testData}
|
|
473
|
+
height={300}
|
|
474
|
+
enableColumnResizing
|
|
475
|
+
onRowClick={onRowClick}
|
|
476
|
+
/>,
|
|
477
|
+
);
|
|
478
|
+
expect(container.querySelector('[role="table"]')).not.toBeNull();
|
|
479
|
+
// If the virtualizer happens to render rows, clicking them calls onRowClick
|
|
480
|
+
const dataRows = Array.from(
|
|
481
|
+
container.querySelectorAll('[role="row"]'),
|
|
482
|
+
).filter((r) => r.getAttribute("aria-rowindex") !== null);
|
|
483
|
+
if (dataRows.length > 0) {
|
|
484
|
+
fireEvent.click(dataRows[0]);
|
|
485
|
+
expect(onRowClick).toHaveBeenCalled();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("empty state renders correctly when data is empty and resizing is enabled", () => {
|
|
490
|
+
const { container } = render(
|
|
491
|
+
<VirtualizedDataTable
|
|
492
|
+
columns={testColumns}
|
|
493
|
+
data={[]}
|
|
494
|
+
height={300}
|
|
495
|
+
enableColumnResizing
|
|
496
|
+
emptyMessage="Nothing here"
|
|
497
|
+
/>,
|
|
498
|
+
);
|
|
499
|
+
expect(container.textContent).toContain("Nothing here");
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ─── Group 9: Barrel re-export of ColumnSizingState ──────────────────────────
|
|
504
|
+
|
|
505
|
+
describe("VirtualizedDataTable — ColumnSizingState barrel re-export", () => {
|
|
506
|
+
it("ColumnSizingState from the barrel index is compatible with the columnSizing prop", () => {
|
|
507
|
+
// This test is intentionally type-level: the import at the top of this file
|
|
508
|
+
// uses `import type { ColumnSizingState } from '../../index'`. If the barrel
|
|
509
|
+
// no longer re-exports it, tsc (run via `pnpm typecheck`) will error.
|
|
510
|
+
// At runtime we just verify the prop is accepted with a well-typed value.
|
|
511
|
+
const sizing: ColumnSizingState = { name: 250 };
|
|
512
|
+
const { container } = render(
|
|
513
|
+
<VirtualizedDataTable
|
|
514
|
+
columns={testColumns}
|
|
515
|
+
data={testData}
|
|
516
|
+
height={300}
|
|
517
|
+
enableColumnResizing
|
|
518
|
+
columnSizing={sizing}
|
|
519
|
+
/>,
|
|
520
|
+
);
|
|
521
|
+
const header = container.querySelectorAll('[role="columnheader"]')[0] as HTMLElement;
|
|
522
|
+
expect(header.style.width).toBe("250px");
|
|
523
|
+
});
|
|
524
|
+
});
|
|
@@ -39,6 +39,8 @@ interface DataTableFilterProps {
|
|
|
39
39
|
selectedFilters: Record<string, string[]>
|
|
40
40
|
onToggleFilter: (categoryId: string, option: string) => void
|
|
41
41
|
className?: string
|
|
42
|
+
/** Minimum number of options before showing the sub-menu search input. Defaults to 8. */
|
|
43
|
+
optionSearchThreshold?: number
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export function DataTableFilter({
|
|
@@ -46,6 +48,7 @@ export function DataTableFilter({
|
|
|
46
48
|
selectedFilters,
|
|
47
49
|
onToggleFilter,
|
|
48
50
|
className,
|
|
51
|
+
optionSearchThreshold = 8,
|
|
49
52
|
}: DataTableFilterProps) {
|
|
50
53
|
const [query, setQuery] = React.useState("")
|
|
51
54
|
const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})
|
|
@@ -139,7 +142,7 @@ export function DataTableFilter({
|
|
|
139
142
|
</DropdownMenuSubTrigger>
|
|
140
143
|
<DropdownMenuSubContent className="max-h-[320px] w-52 overflow-y-auto p-1">
|
|
141
144
|
{/* Submenu search — only for categories with many options */}
|
|
142
|
-
{category.options.length >
|
|
145
|
+
{category.options.length > optionSearchThreshold && (
|
|
143
146
|
<div className="sticky top-0 z-10 border-b border-border bg-popover p-1.5">
|
|
144
147
|
<div className="relative">
|
|
145
148
|
<Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
|
|
@@ -151,7 +154,14 @@ export function DataTableFilter({
|
|
|
151
154
|
setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))
|
|
152
155
|
}
|
|
153
156
|
onClick={(e) => e.stopPropagation()}
|
|
154
|
-
onKeyDown={(e) =>
|
|
157
|
+
onKeyDown={(e) => {
|
|
158
|
+
// Allow navigation keys to propagate to Radix menu handling
|
|
159
|
+
// so keyboard users can move to and select filtered options.
|
|
160
|
+
const navKeys = ["ArrowDown", "ArrowUp", "Enter", "Escape", "Tab"]
|
|
161
|
+
if (!navKeys.includes(e.key)) {
|
|
162
|
+
e.stopPropagation()
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
155
165
|
/>
|
|
156
166
|
</div>
|
|
157
167
|
</div>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type SortingState,
|
|
11
11
|
type ColumnFiltersState,
|
|
12
12
|
type VisibilityState,
|
|
13
|
+
type ColumnSizingState,
|
|
13
14
|
type OnChangeFn,
|
|
14
15
|
} from "@tanstack/react-table"
|
|
15
16
|
import { ArrowDown, ArrowUp, ArrowUpDown, SearchX, Loader2 } from "lucide-react"
|
|
@@ -36,6 +37,12 @@ export interface VirtualizedDataTableProps<TData> {
|
|
|
36
37
|
hasMore?: boolean
|
|
37
38
|
isFetchingMore?: boolean
|
|
38
39
|
|
|
40
|
+
// Column resizing
|
|
41
|
+
enableColumnResizing?: boolean
|
|
42
|
+
columnResizeMode?: "onChange" | "onEnd"
|
|
43
|
+
columnSizing?: ColumnSizingState
|
|
44
|
+
onColumnSizingChange?: OnChangeFn<ColumnSizingState>
|
|
45
|
+
|
|
39
46
|
// Server-driven state (controlled) — omit for internal state
|
|
40
47
|
sorting?: SortingState
|
|
41
48
|
onSortingChange?: OnChangeFn<SortingState>
|
|
@@ -66,6 +73,10 @@ export function VirtualizedDataTable<TData>({
|
|
|
66
73
|
reachBottomThreshold = 5,
|
|
67
74
|
hasMore = true,
|
|
68
75
|
isFetchingMore,
|
|
76
|
+
enableColumnResizing = false,
|
|
77
|
+
columnResizeMode = "onEnd",
|
|
78
|
+
columnSizing,
|
|
79
|
+
onColumnSizingChange,
|
|
69
80
|
sorting,
|
|
70
81
|
onSortingChange,
|
|
71
82
|
columnFilters,
|
|
@@ -97,6 +108,13 @@ export function VirtualizedDataTable<TData>({
|
|
|
97
108
|
const resolvedOnColumnVisibilityChange =
|
|
98
109
|
onColumnVisibilityChange ?? setInternalColumnVisibility
|
|
99
110
|
|
|
111
|
+
// Controlled/uncontrolled state for column sizing
|
|
112
|
+
const [internalColumnSizing, setInternalColumnSizing] =
|
|
113
|
+
React.useState<ColumnSizingState>({})
|
|
114
|
+
const resolvedColumnSizing = columnSizing ?? internalColumnSizing
|
|
115
|
+
const resolvedOnColumnSizingChange =
|
|
116
|
+
onColumnSizingChange ?? setInternalColumnSizing
|
|
117
|
+
|
|
100
118
|
// TanStack Table setup
|
|
101
119
|
const table = useReactTable({
|
|
102
120
|
data,
|
|
@@ -106,10 +124,14 @@ export function VirtualizedDataTable<TData>({
|
|
|
106
124
|
sorting: resolvedSorting,
|
|
107
125
|
columnFilters: resolvedColumnFilters,
|
|
108
126
|
columnVisibility: resolvedColumnVisibility,
|
|
127
|
+
columnSizing: resolvedColumnSizing,
|
|
109
128
|
},
|
|
110
129
|
onSortingChange: resolvedOnSortingChange,
|
|
111
130
|
onColumnFiltersChange: resolvedOnColumnFiltersChange,
|
|
112
131
|
onColumnVisibilityChange: resolvedOnColumnVisibilityChange,
|
|
132
|
+
onColumnSizingChange: resolvedOnColumnSizingChange,
|
|
133
|
+
enableColumnResizing,
|
|
134
|
+
columnResizeMode,
|
|
113
135
|
manualSorting: true,
|
|
114
136
|
manualFiltering: true,
|
|
115
137
|
manualPagination: true,
|
|
@@ -188,7 +210,10 @@ export function VirtualizedDataTable<TData>({
|
|
|
188
210
|
{headerGroup.headers.map((header, colIdx) => (
|
|
189
211
|
<div
|
|
190
212
|
key={header.id}
|
|
191
|
-
className=
|
|
213
|
+
className={cn(
|
|
214
|
+
"h-9 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative",
|
|
215
|
+
header.column.getCanResize() && "pr-4",
|
|
216
|
+
)}
|
|
192
217
|
style={{
|
|
193
218
|
width: header.getSize(),
|
|
194
219
|
minWidth: header.getSize(),
|
|
@@ -229,6 +254,20 @@ export function VirtualizedDataTable<TData>({
|
|
|
229
254
|
header.getContext(),
|
|
230
255
|
)
|
|
231
256
|
)}
|
|
257
|
+
{header.column.getCanResize() && (
|
|
258
|
+
<div
|
|
259
|
+
onMouseDown={header.getResizeHandler()}
|
|
260
|
+
onTouchStart={header.getResizeHandler()}
|
|
261
|
+
className={cn(
|
|
262
|
+
"absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
|
|
263
|
+
"after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
|
|
264
|
+
"after:bg-transparent hover:after:bg-primary/30",
|
|
265
|
+
header.column.getIsResizing() && "after:bg-primary/50",
|
|
266
|
+
)}
|
|
267
|
+
role="separator"
|
|
268
|
+
aria-orientation="vertical"
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
232
271
|
</div>
|
|
233
272
|
))}
|
|
234
273
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,7 @@ export * from "./components/tooltip"
|
|
|
91
91
|
export * from "./components/variable-autocomplete"
|
|
92
92
|
export * from "./components/view-mode-toggle"
|
|
93
93
|
export * from "./components/virtualized-data-table"
|
|
94
|
+
export type { ColumnSizingState } from "@tanstack/react-table"
|
|
94
95
|
|
|
95
96
|
// Charts (re-exported for backward compatibility with root imports)
|
|
96
97
|
export * from "./charts/index"
|