@mendylanda/ui 0.1.9 → 0.3.0-alpha.0
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/NOTICE.md +1 -1
- package/README.md +32 -3
- package/dist/class-names.js +1 -1
- package/dist/customization.js +1 -1
- package/dist/filters/filter-bar.js +3 -0
- package/dist/filters/filter-definition.js +1 -0
- package/dist/filters/filter-menu-panel.js +1 -1
- package/dist/filters/filter-select-editor.d.ts +8 -2
- package/dist/filters/filter-select-editor.js +11 -7
- package/dist/styles.css +179 -0
- package/dist/styles.tailwind3.css +179 -0
- package/dist/table/clipboard.d.ts +6 -0
- package/dist/table/clipboard.js +51 -0
- package/dist/table/columns.d.ts +60 -0
- package/dist/table/columns.js +72 -0
- package/dist/table/data-table.d.ts +14 -0
- package/dist/table/data-table.js +15 -0
- package/dist/table/features.d.ts +34 -0
- package/dist/table/features.js +35 -0
- package/dist/table/index.d.ts +9 -0
- package/dist/table/index.js +9 -0
- package/dist/table/state.d.ts +31 -0
- package/dist/table/state.js +48 -0
- package/dist/table/table-controls.d.ts +23 -0
- package/dist/table/table-controls.js +95 -0
- package/dist/table/table-feedback.d.ts +7 -0
- package/dist/table/table-feedback.js +11 -0
- package/dist/table/table-parts.d.ts +24 -0
- package/dist/table/table-parts.js +67 -0
- package/dist/table/table-view.d.ts +35 -0
- package/dist/table/table-view.js +110 -0
- package/dist/table/use-data-table.d.ts +24 -0
- package/dist/table/use-data-table.js +100 -0
- package/dist/table/use-result-selection.d.ts +17 -0
- package/dist/table/use-result-selection.js +53 -0
- package/dist/table/use-table-interaction.d.ts +15 -0
- package/dist/table/use-table-interaction.js +125 -0
- package/dist/table/use-viewport-width.d.ts +3 -0
- package/dist/table/use-viewport-width.js +17 -0
- package/package.json +8 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
export function useResultSelection({ scope, rowIds, totalCount, }) {
|
|
4
|
+
const [stored, setStored] = useState({
|
|
5
|
+
key: scope.key,
|
|
6
|
+
value: { mode: "ids", ids: [] },
|
|
7
|
+
});
|
|
8
|
+
if (stored.key !== scope.key) {
|
|
9
|
+
setStored({ key: scope.key, value: { mode: "ids", ids: [] } });
|
|
10
|
+
}
|
|
11
|
+
const selection = stored.key === scope.key ? stored.value : { mode: "ids", ids: [] };
|
|
12
|
+
const ids = new Set(selection.mode === "ids" ? selection.ids : selection.excludedIds);
|
|
13
|
+
const rowSelection = Object.fromEntries(rowIds
|
|
14
|
+
.filter((id) => (selection.mode === "ids" ? ids.has(id) : !ids.has(id)))
|
|
15
|
+
.map((id) => [id, true]));
|
|
16
|
+
function onRowSelectionChange(updater) {
|
|
17
|
+
setStored((previous) => {
|
|
18
|
+
const value = previous.key === scope.key ? previous.value : { mode: "ids", ids: [] };
|
|
19
|
+
const selected = new Set(value.mode === "ids" ? value.ids : value.excludedIds);
|
|
20
|
+
const current = Object.fromEntries(rowIds
|
|
21
|
+
.filter((id) => (value.mode === "ids" ? selected.has(id) : !selected.has(id)))
|
|
22
|
+
.map((id) => [id, true]));
|
|
23
|
+
const next = typeof updater === "function" ? updater(current) : updater;
|
|
24
|
+
for (const id of rowIds) {
|
|
25
|
+
if (value.mode === "ids" ? next[id] : !next[id])
|
|
26
|
+
selected.add(id);
|
|
27
|
+
else
|
|
28
|
+
selected.delete(id);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
key: scope.key,
|
|
32
|
+
value: value.mode === "ids"
|
|
33
|
+
? { mode: "ids", ids: [...selected] }
|
|
34
|
+
: { ...value, excludedIds: [...selected] },
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
selection,
|
|
40
|
+
rowSelection,
|
|
41
|
+
onRowSelectionChange,
|
|
42
|
+
count: selection.mode === "ids"
|
|
43
|
+
? selection.ids.length
|
|
44
|
+
: totalCount == null
|
|
45
|
+
? undefined
|
|
46
|
+
: Math.max(0, totalCount - selection.excludedIds.length),
|
|
47
|
+
clear: () => setStored({ key: scope.key, value: { mode: "ids", ids: [] } }),
|
|
48
|
+
selectAllMatching: () => setStored({
|
|
49
|
+
key: scope.key,
|
|
50
|
+
value: { mode: "matching", scope: scope.value, excludedIds: [] },
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { KeyboardEvent, RefObject } from "react";
|
|
2
|
+
import type { DataTableInstance } from "./use-data-table.js";
|
|
3
|
+
export declare function useTableInteraction<T extends object>({ table, pinningActive, container, queryKey, onRowActivate, onCopyError, scrollToIndex, }: {
|
|
4
|
+
table: DataTableInstance<T>;
|
|
5
|
+
pinningActive: boolean;
|
|
6
|
+
container: RefObject<HTMLDivElement | null>;
|
|
7
|
+
queryKey?: string;
|
|
8
|
+
onRowActivate?: (row: T) => void;
|
|
9
|
+
onCopyError?: (error: unknown) => void;
|
|
10
|
+
scrollToIndex: (index: number) => void;
|
|
11
|
+
}): {
|
|
12
|
+
announcement: string;
|
|
13
|
+
copied: boolean;
|
|
14
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { interactiveSelector, selectedCellsText } from "./clipboard.js";
|
|
4
|
+
export function useTableInteraction({ table, pinningActive, container, queryKey, onRowActivate, onCopyError, scrollToIndex, }) {
|
|
5
|
+
const rows = table.getRowModel().rows;
|
|
6
|
+
const columns = [
|
|
7
|
+
...table.getStartVisibleLeafColumns(),
|
|
8
|
+
...table.getCenterVisibleLeafColumns(),
|
|
9
|
+
...table.getEndVisibleLeafColumns(),
|
|
10
|
+
];
|
|
11
|
+
const focused = table.getFocusedCell();
|
|
12
|
+
const [announcement, setAnnouncement] = useState("");
|
|
13
|
+
const [copied, setCopied] = useState(false);
|
|
14
|
+
const copyTimer = useRef(undefined);
|
|
15
|
+
const mounted = useRef(true);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
mounted.current = true;
|
|
18
|
+
return () => {
|
|
19
|
+
mounted.current = false;
|
|
20
|
+
clearTimeout(copyTimer.current);
|
|
21
|
+
};
|
|
22
|
+
}, []);
|
|
23
|
+
const resetCellSelection = table.resetCellSelection;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
resetCellSelection(true);
|
|
26
|
+
if (container.current)
|
|
27
|
+
container.current.scrollTop = 0;
|
|
28
|
+
}, [queryKey, resetCellSelection, container]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const dismiss = (event) => {
|
|
31
|
+
if (container.current && !container.current.contains(event.target))
|
|
32
|
+
resetCellSelection(true);
|
|
33
|
+
};
|
|
34
|
+
document.addEventListener("pointerdown", dismiss);
|
|
35
|
+
return () => document.removeEventListener("pointerdown", dismiss);
|
|
36
|
+
}, [resetCellSelection, container]);
|
|
37
|
+
async function copySelection() {
|
|
38
|
+
try {
|
|
39
|
+
const text = selectedCellsText(table);
|
|
40
|
+
if (!text)
|
|
41
|
+
return;
|
|
42
|
+
await navigator.clipboard.writeText(text);
|
|
43
|
+
if (!mounted.current)
|
|
44
|
+
return;
|
|
45
|
+
setAnnouncement("Selected cells copied");
|
|
46
|
+
setCopied(true);
|
|
47
|
+
clearTimeout(copyTimer.current);
|
|
48
|
+
copyTimer.current = setTimeout(() => setCopied(false), 300);
|
|
49
|
+
}
|
|
50
|
+
catch (reason) {
|
|
51
|
+
if (mounted.current) {
|
|
52
|
+
setAnnouncement("Could not copy selected cells");
|
|
53
|
+
onCopyError?.(reason);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function focusCell(rowId, columnId) {
|
|
58
|
+
const index = rows.findIndex((row) => row.id === rowId);
|
|
59
|
+
if (index < 0)
|
|
60
|
+
return;
|
|
61
|
+
scrollToIndex(index);
|
|
62
|
+
requestAnimationFrame(() => {
|
|
63
|
+
const cell = container.current?.querySelector(`[data-row-id="${CSS.escape(rowId)}"][data-column-id="${CSS.escape(columnId)}"]`);
|
|
64
|
+
cell?.focus({ preventScroll: true });
|
|
65
|
+
if (cell && (!pinningActive || !table.getColumn(columnId)?.getIsPinned())) {
|
|
66
|
+
const box = cell.getBoundingClientRect(), viewport = container.current.getBoundingClientRect();
|
|
67
|
+
const startWidth = !pinningActive
|
|
68
|
+
? 0
|
|
69
|
+
: table.getStartVisibleLeafColumns().reduce((sum, col) => sum + col.getSize(), 0);
|
|
70
|
+
const endWidth = !pinningActive
|
|
71
|
+
? 0
|
|
72
|
+
: table.getEndVisibleLeafColumns().reduce((sum, col) => sum + col.getSize(), 0);
|
|
73
|
+
if (box.left < viewport.left + startWidth)
|
|
74
|
+
container.current.scrollLeft += box.left - viewport.left - startWidth;
|
|
75
|
+
else if (box.right > viewport.right - endWidth)
|
|
76
|
+
container.current.scrollLeft += box.right - viewport.right + endWidth;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const onKeyDown = (event) => {
|
|
81
|
+
if (event.target.closest(interactiveSelector))
|
|
82
|
+
return;
|
|
83
|
+
if ((event.ctrlKey || event.metaKey) &&
|
|
84
|
+
event.key.toLowerCase() === "c" &&
|
|
85
|
+
!window.getSelection()?.toString() &&
|
|
86
|
+
table.getSelectedCellCount()) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
void copySelection();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
table.selectAllCells();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (event.key === "Escape") {
|
|
97
|
+
table.resetCellSelection(true);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const directions = {
|
|
101
|
+
ArrowUp: "up",
|
|
102
|
+
ArrowDown: "down",
|
|
103
|
+
ArrowLeft: "left",
|
|
104
|
+
ArrowRight: "right",
|
|
105
|
+
};
|
|
106
|
+
const direction = directions[event.key];
|
|
107
|
+
if (direction) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
if (!table.getFocusedCell() && rows[0] && columns[0])
|
|
110
|
+
table.setFocusedCell(rows[0].id, columns[0].id);
|
|
111
|
+
else if (event.shiftKey)
|
|
112
|
+
table.extendCellSelection(direction);
|
|
113
|
+
else
|
|
114
|
+
table.moveCellSelection(direction);
|
|
115
|
+
const range = table.atoms.cellSelection.get().at(-1);
|
|
116
|
+
if (range)
|
|
117
|
+
focusCell(range.focusRowId, range.focusColumnId);
|
|
118
|
+
}
|
|
119
|
+
else if (event.key === "Enter" && focused && onRowActivate) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
onRowActivate(focused.row.original);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return { announcement, copied, onKeyDown };
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect, useState } from "react";
|
|
3
|
+
/** Vertical virtualization does not notify React when only the viewport width changes. */
|
|
4
|
+
export function useViewportWidth(container) {
|
|
5
|
+
const [width, setWidth] = useState(null);
|
|
6
|
+
useLayoutEffect(() => {
|
|
7
|
+
const element = container.current;
|
|
8
|
+
if (!element)
|
|
9
|
+
return;
|
|
10
|
+
const measure = () => setWidth(element.clientWidth);
|
|
11
|
+
measure();
|
|
12
|
+
const observer = new ResizeObserver(measure);
|
|
13
|
+
observer.observe(element);
|
|
14
|
+
return () => observer.disconnect();
|
|
15
|
+
}, [container]);
|
|
16
|
+
return width;
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mendylanda/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Mendy Landa\u2019s reusable React components. Typed filters with
|
|
3
|
+
"version": "0.3.0-alpha.0",
|
|
4
|
+
"description": "Mendy Landa\u2019s reusable React components. Typed filters and tables with shared interaction behavior and customizable UI.",
|
|
5
5
|
"homepage": "https://ui.mendylanda.com",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Mendy Landa",
|
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
"./styles.tailwind3.css": {
|
|
62
62
|
"types": "./dist/styles.css.d.ts",
|
|
63
63
|
"default": "./dist/styles.tailwind3.css"
|
|
64
|
+
},
|
|
65
|
+
"./table": {
|
|
66
|
+
"types": "./dist/table/index.d.ts",
|
|
67
|
+
"import": "./dist/table/index.js",
|
|
68
|
+
"default": "./dist/table/index.js"
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
"publishConfig": {
|
|
@@ -76,6 +81,7 @@
|
|
|
76
81
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
77
82
|
"@radix-ui/react-label": "^2.1.7",
|
|
78
83
|
"@radix-ui/react-slot": "^1.2.3",
|
|
84
|
+
"@tanstack/react-table": "9.2.4",
|
|
79
85
|
"@tanstack/react-virtual": "^3.14.11",
|
|
80
86
|
"class-variance-authority": "^0.7.1",
|
|
81
87
|
"clsx": "^2.1.1",
|