@react-fabric/data 1.0.0-beta.5 → 1.0.0-beta.52
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/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
- package/css/styles.css +24 -0
- package/dist/cjs/checklist/CheckList.js +48 -16
- package/dist/cjs/histogram/Histogram.js +48 -10
- package/dist/cjs/hooks/useFilteredList.js +20 -0
- package/dist/cjs/hooks/usePagination.js +27 -4
- package/dist/cjs/hooks/useSelectableList.js +29 -0
- package/dist/cjs/i18n/ar.json.js +8 -1
- package/dist/cjs/i18n/en.json.js +8 -1
- package/dist/cjs/json/JsonViewer.js +67 -27
- package/dist/cjs/pagination/Pagination.js +10 -0
- package/dist/cjs/table/AddColumn.js +1 -1
- package/dist/cjs/table/BodyCell.js +38 -5
- package/dist/cjs/table/BodyRow.js +107 -0
- package/dist/cjs/table/CheckboxCell.js +1 -1
- package/dist/cjs/table/ColumnFilters.js +97 -0
- package/dist/cjs/table/HeaderCell.js +70 -37
- package/dist/cjs/table/Table.js +113 -71
- package/dist/cjs/table/types.js +29 -0
- package/dist/cjs/table/useResizer.js +4 -4
- package/dist/cjs/table/useVirtualTable.js +122 -18
- package/dist/cjs/tree/TreeNode.js +45 -25
- package/dist/cjs/tree/TreePanel.js +28 -10
- package/dist/cjs/tree/useTree.js +97 -15
- package/dist/cjs/tree/utils.js +20 -5
- package/dist/cjs/virtual/Gallery.js +84 -35
- package/dist/cjs/virtual/List.js +82 -32
- package/dist/esm/checklist/CheckList.js +50 -18
- package/dist/esm/histogram/Histogram.js +49 -11
- package/dist/esm/hooks/useFilteredList.js +20 -0
- package/dist/esm/hooks/usePagination.js +27 -4
- package/dist/esm/hooks/useSelectableList.js +29 -0
- package/dist/esm/i18n/ar.json.js +8 -2
- package/dist/esm/i18n/en.json.js +8 -2
- package/dist/esm/json/JsonViewer.js +68 -28
- package/dist/esm/pagination/Pagination.js +10 -0
- package/dist/esm/table/AddColumn.js +1 -1
- package/dist/esm/table/BodyCell.js +38 -5
- package/dist/esm/table/BodyRow.js +107 -0
- package/dist/esm/table/CheckboxCell.js +1 -1
- package/dist/esm/table/ColumnFilters.js +97 -0
- package/dist/esm/table/HeaderCell.js +71 -38
- package/dist/esm/table/Table.js +115 -73
- package/dist/esm/table/types.js +29 -0
- package/dist/esm/table/useResizer.js +4 -4
- package/dist/esm/table/useVirtualTable.js +124 -20
- package/dist/esm/tree/TreeNode.js +47 -27
- package/dist/esm/tree/TreePanel.js +28 -10
- package/dist/esm/tree/useTree.js +99 -17
- package/dist/esm/tree/utils.js +20 -5
- package/dist/esm/virtual/Gallery.js +84 -35
- package/dist/esm/virtual/List.js +82 -32
- package/dist/types/checklist/CheckList.d.ts +24 -2
- package/dist/types/histogram/Histogram.d.ts +31 -4
- package/dist/types/hooks/useFilteredList.d.ts +20 -2
- package/dist/types/hooks/usePagination.d.ts +19 -0
- package/dist/types/hooks/useSelectableList.d.ts +29 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/json/JsonViewer.d.ts +27 -2
- package/dist/types/pagination/Pagination.d.ts +10 -1
- package/dist/types/table/AddColumn.d.ts +2 -2
- package/dist/types/table/BodyCell.d.ts +4 -3
- package/dist/types/table/BodyRow.d.ts +20 -0
- package/dist/types/table/ColumnFilters.d.ts +9 -0
- package/dist/types/table/EmptyCell.d.ts +2 -2
- package/dist/types/table/HeaderCell.d.ts +2 -2
- package/dist/types/table/Table.d.ts +14 -1
- package/dist/types/table/types.d.ts +48 -7
- package/dist/types/table/useTableColumns.d.ts +6 -6
- package/dist/types/table/useVirtualTable.d.ts +23 -8
- package/dist/types/tree/TreeNode.d.ts +1 -1
- package/dist/types/tree/TreePanel.d.ts +42 -1
- package/dist/types/tree/types.d.ts +25 -5
- package/dist/types/tree/useTree.d.ts +3 -10
- package/dist/types/tree/utils.d.ts +4 -1
- package/dist/types/virtual/Gallery.d.ts +44 -4
- package/dist/types/virtual/List.d.ts +44 -4
- package/package.json +13 -10
- package/src/checklist/CheckList.tsx +103 -77
- package/src/histogram/Histogram.tsx +45 -11
- package/src/hooks/useFilteredList.tsx +19 -0
- package/src/hooks/usePagination.tsx +32 -7
- package/src/hooks/useSelectableList.ts +29 -0
- package/src/i18n/ar.json +5 -0
- package/src/i18n/en.json +5 -0
- package/src/index.ts +1 -1
- package/src/json/JsonViewer.tsx +71 -35
- package/src/pagination/Pagination.tsx +10 -0
- package/src/table/AddColumn.tsx +3 -3
- package/src/table/BodyCell.tsx +34 -5
- package/src/table/BodyRow.tsx +143 -0
- package/src/table/CheckboxCell.tsx +1 -1
- package/src/table/ColumnFilters.tsx +105 -0
- package/src/table/EmptyCell.tsx +2 -2
- package/src/table/HeaderCell.tsx +72 -14
- package/src/table/Table.tsx +177 -77
- package/src/table/types.ts +52 -6
- package/src/table/useResizer.ts +5 -3
- package/src/table/useTableColumns.ts +7 -7
- package/src/table/useVirtualTable.ts +146 -24
- package/src/tree/TreeNode.tsx +98 -56
- package/src/tree/TreePanel.tsx +64 -3
- package/src/tree/types.ts +32 -4
- package/src/tree/useTree.ts +109 -26
- package/src/tree/utils.ts +23 -8
- package/src/virtual/Gallery.tsx +96 -42
- package/src/virtual/List.tsx +82 -33
- package/dist/cjs/histogram/Histogram.module.css +0 -1
- package/dist/esm/histogram/Histogram.module.css +0 -1
|
@@ -21,78 +21,200 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { compareValues, groupBy } from "@react-fabric/utilities";
|
|
25
|
+
import {
|
|
26
|
+
defaultRangeExtractor,
|
|
27
|
+
type Range,
|
|
28
|
+
useVirtualizer,
|
|
29
|
+
} from "@tanstack/react-virtual";
|
|
30
|
+
import {
|
|
31
|
+
useCallback,
|
|
32
|
+
useEffect,
|
|
33
|
+
useMemo,
|
|
34
|
+
useReducer,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from "react";
|
|
26
38
|
|
|
27
|
-
interface DataState {
|
|
28
|
-
|
|
39
|
+
interface DataState<T = KeyValue> {
|
|
40
|
+
data: T[];
|
|
41
|
+
items: T[];
|
|
42
|
+
grouped: KeyValue<T[]>;
|
|
43
|
+
groups: AnyObject[];
|
|
44
|
+
expanded: AnyObject[];
|
|
45
|
+
checked: AnyObject[];
|
|
29
46
|
allChecked: 0 | 1 | 2;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
type DataAction =
|
|
49
|
+
type DataAction =
|
|
50
|
+
| { type: "updateState"; checked: AnyObject[] }
|
|
51
|
+
| { type: "groupExpand"; key: AnyObject }
|
|
52
|
+
| { type: "reset"; key: AnyObject; data: AnyObject[] }
|
|
53
|
+
| { type: "expand"; key: AnyObject }
|
|
54
|
+
| { type: "check"; key: AnyObject }
|
|
55
|
+
| { type: "checkall" };
|
|
33
56
|
|
|
34
|
-
export const useVirtualTable = (
|
|
35
|
-
data:
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
export const useVirtualTable = <T extends KeyValue = KeyValue>(
|
|
58
|
+
data: T[],
|
|
59
|
+
options: {
|
|
60
|
+
checked?: AnyObject[];
|
|
61
|
+
keyProperty: keyof T;
|
|
62
|
+
groupProperty?: keyof T;
|
|
63
|
+
onCheckChanged?: (checked: AnyObject[]) => void;
|
|
64
|
+
},
|
|
38
65
|
) => {
|
|
39
66
|
// TODO: refactor data into item list, apply grouping if groupColumn provided
|
|
40
67
|
// TODO: when grouping maintain map of group with children, {___GROUP___:true, label, items, open}
|
|
41
68
|
|
|
42
|
-
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
43
|
-
const virtualizer = useVirtualizer({
|
|
44
|
-
count: data.length,
|
|
45
|
-
getScrollElement: () => scrollerRef.current,
|
|
46
|
-
estimateSize: () => 24,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
69
|
const [state, dispatch] = useReducer(
|
|
50
|
-
(state: DataState
|
|
70
|
+
(state: DataState<T>, action: DataAction) => {
|
|
71
|
+
if (action.type === "updateState") {
|
|
72
|
+
state.checked = action.checked;
|
|
73
|
+
}
|
|
74
|
+
if (action.type === "expand") {
|
|
75
|
+
if (state.expanded.includes(action.key))
|
|
76
|
+
state.expanded.splice(state.expanded.indexOf(action.key), 1);
|
|
77
|
+
else state.expanded.push(action.key);
|
|
78
|
+
}
|
|
51
79
|
if (action.type === "check") {
|
|
52
80
|
if (state.checked.includes(action.key))
|
|
53
81
|
state.checked.splice(state.checked.indexOf(action.key), 1);
|
|
54
82
|
else state.checked.push(action.key);
|
|
83
|
+
options.onCheckChanged?.(state.checked);
|
|
55
84
|
}
|
|
56
85
|
if (action.type === "checkall") {
|
|
57
86
|
if (state.allChecked !== 0) {
|
|
58
87
|
state.checked = [];
|
|
59
|
-
} else if (keyProperty) {
|
|
60
|
-
state.checked = data.map((item) => item[keyProperty]);
|
|
88
|
+
} else if (options.keyProperty) {
|
|
89
|
+
state.checked = state.data.map((item) => item[options.keyProperty]);
|
|
90
|
+
}
|
|
91
|
+
options.onCheckChanged?.(state.checked);
|
|
92
|
+
}
|
|
93
|
+
const checkList = state.data.filter((hit) =>
|
|
94
|
+
state.checked.includes(hit[options.keyProperty]),
|
|
95
|
+
);
|
|
96
|
+
if (action.type === "groupExpand") {
|
|
97
|
+
const group = state.groups.find((group) => group.key === action.key);
|
|
98
|
+
if (group) {
|
|
99
|
+
group.open = !group.open;
|
|
100
|
+
state.groups = state.groups.slice();
|
|
101
|
+
state.items = state.groups
|
|
102
|
+
.map((group) => [
|
|
103
|
+
group,
|
|
104
|
+
group.open ? state.grouped[group.key] : undefined,
|
|
105
|
+
])
|
|
106
|
+
.flat(2)
|
|
107
|
+
.filter(Boolean) as any;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (action.type === "reset") {
|
|
111
|
+
state.data = action.data;
|
|
112
|
+
if (action.key) {
|
|
113
|
+
state.grouped = groupBy<T>(state.data, action.key);
|
|
114
|
+
state.groups = Object.keys(state.grouped)
|
|
115
|
+
.sort(compareValues("asc"))
|
|
116
|
+
.map((group) => ({
|
|
117
|
+
__GROUP__: true,
|
|
118
|
+
open: true,
|
|
119
|
+
key: group,
|
|
120
|
+
items: state.grouped[group],
|
|
121
|
+
itemCount: state.grouped[group].length,
|
|
122
|
+
}));
|
|
123
|
+
state.items = state.groups
|
|
124
|
+
.map((group) => [group, ...state.grouped[group.key]])
|
|
125
|
+
.flat() as any;
|
|
126
|
+
} else {
|
|
127
|
+
state.items = state.data;
|
|
61
128
|
}
|
|
62
129
|
}
|
|
63
|
-
onCheckChanged?.(state.checked);
|
|
64
130
|
return {
|
|
65
131
|
...state,
|
|
66
132
|
allChecked:
|
|
67
|
-
|
|
133
|
+
checkList.length === state.data.length
|
|
68
134
|
? 1
|
|
69
135
|
: state.checked.length
|
|
70
136
|
? 2
|
|
71
137
|
: 0,
|
|
72
|
-
} as DataState
|
|
138
|
+
} as DataState<T>;
|
|
73
139
|
},
|
|
74
140
|
{
|
|
141
|
+
data,
|
|
142
|
+
items: [],
|
|
143
|
+
grouped: {},
|
|
144
|
+
groups: [],
|
|
75
145
|
checked: [],
|
|
146
|
+
expanded: [],
|
|
76
147
|
allChecked: 0,
|
|
77
148
|
},
|
|
78
149
|
);
|
|
79
150
|
|
|
151
|
+
const activeStickyIndexRef = useRef(0);
|
|
152
|
+
const stickyIndexes = useMemo<number[]>(
|
|
153
|
+
() =>
|
|
154
|
+
state.items
|
|
155
|
+
.map((hit, idx) => ("__GROUP__" in hit ? idx : undefined))
|
|
156
|
+
.filter(Boolean) as number[],
|
|
157
|
+
[state.items],
|
|
158
|
+
);
|
|
159
|
+
const isActiveSticky = (index: number) =>
|
|
160
|
+
activeStickyIndexRef.current === index;
|
|
161
|
+
|
|
162
|
+
const [hasActiveSticky, setHasActiveSticky] = useState(false);
|
|
163
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
164
|
+
const virtualizer = useVirtualizer({
|
|
165
|
+
count: state.items.length,
|
|
166
|
+
getScrollElement: () => scrollerRef.current,
|
|
167
|
+
estimateSize: () => 32,
|
|
168
|
+
rangeExtractor: useCallback(
|
|
169
|
+
(range: Range) => {
|
|
170
|
+
activeStickyIndexRef.current =
|
|
171
|
+
[...stickyIndexes]
|
|
172
|
+
.reverse()
|
|
173
|
+
.find((index) => range.startIndex > index) ?? 0;
|
|
174
|
+
|
|
175
|
+
setHasActiveSticky(range.startIndex !== activeStickyIndexRef.current);
|
|
176
|
+
|
|
177
|
+
const next = new Set([
|
|
178
|
+
activeStickyIndexRef.current,
|
|
179
|
+
...defaultRangeExtractor(range),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
return [...next];
|
|
183
|
+
},
|
|
184
|
+
[stickyIndexes],
|
|
185
|
+
),
|
|
186
|
+
});
|
|
187
|
+
|
|
80
188
|
const virtualItems = virtualizer.getVirtualItems();
|
|
81
189
|
|
|
82
190
|
const getData = useCallback(
|
|
83
191
|
(index: number) => {
|
|
84
|
-
return
|
|
192
|
+
return state.items[index];
|
|
85
193
|
},
|
|
86
|
-
[
|
|
194
|
+
[state.items],
|
|
87
195
|
);
|
|
88
196
|
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
dispatch({ type: "reset", key: options.groupProperty, data });
|
|
199
|
+
}, [data, options.groupProperty]);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
dispatch({ type: "updateState", checked: options.checked ?? [] });
|
|
203
|
+
}, [options.checked]);
|
|
204
|
+
|
|
89
205
|
return {
|
|
90
206
|
scrollerRef,
|
|
91
207
|
items: virtualItems,
|
|
92
208
|
checkState: state,
|
|
93
209
|
getData,
|
|
210
|
+
virtualizer,
|
|
211
|
+
hasActiveSticky,
|
|
212
|
+
isActiveSticky,
|
|
213
|
+
top: virtualizer.range?.startIndex,
|
|
94
214
|
totalSize: virtualizer.getTotalSize,
|
|
95
215
|
measureElement: virtualizer.measureElement,
|
|
216
|
+
toggleGroupExpand: (key: Key) => dispatch({ type: "groupExpand", key }),
|
|
217
|
+
toggleExpand: (key: Key) => dispatch({ type: "expand", key }),
|
|
96
218
|
toggleChecked: (key: Key) => dispatch({ type: "check", key }),
|
|
97
219
|
toggleAllChecked: () => dispatch({ type: "checkall" }),
|
|
98
220
|
};
|
package/src/tree/TreeNode.tsx
CHANGED
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { Badge, CoreIcons, getBadgeProps, Icon } from "@react-fabric/core";
|
|
25
|
+
import { isEmpty, isString } from "@react-fabric/utilities";
|
|
25
26
|
import classNames from "classnames";
|
|
26
|
-
import { useMemo } from "react";
|
|
27
|
+
import { isValidElement, useMemo } from "react";
|
|
28
|
+
import { useTranslation } from "react-i18next";
|
|
27
29
|
import { type TreeNodeProps } from "./types";
|
|
28
30
|
|
|
29
31
|
export const TreeNode = ({
|
|
@@ -35,30 +37,43 @@ export const TreeNode = ({
|
|
|
35
37
|
defaultLeafIcon,
|
|
36
38
|
defaultNodeIcon,
|
|
37
39
|
onToggle,
|
|
40
|
+
onClick,
|
|
38
41
|
onSelect,
|
|
39
42
|
onChecked,
|
|
40
43
|
children,
|
|
44
|
+
leafClassName,
|
|
45
|
+
nodeClassName,
|
|
41
46
|
}: TreeNodeProps) => {
|
|
47
|
+
const { t } = useTranslation("data");
|
|
42
48
|
const badgeProps = useMemo(() => {
|
|
43
49
|
return getBadgeProps(node.badge);
|
|
44
50
|
}, [node.badge]);
|
|
45
51
|
|
|
46
52
|
const filler = useMemo(() => {
|
|
47
|
-
return node.lines.slice(
|
|
48
|
-
<
|
|
53
|
+
return node.lines.slice(1).map((line, idx) => (
|
|
54
|
+
<div
|
|
49
55
|
key={idx}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<path d="M14.989 12.389l0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v14.1l0.161 0.389 0.389 0.161h6.6z" />
|
|
57
|
-
)}
|
|
58
|
-
{!noLines && line === 3 && (
|
|
59
|
-
<path d="M8.55 12.55h6.050l0.389-0.161 0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v28.2h1.1z" />
|
|
56
|
+
className={classNames(
|
|
57
|
+
"flex-initial self-stretch flex items-center w-6 relative",
|
|
58
|
+
!noLines &&
|
|
59
|
+
line > 0 &&
|
|
60
|
+
"before:absolute before:top-0 before:start-[7px] before:bg-tint-200 before:min-h-[50%] before:w-[2px] before:scale-x-[.625]",
|
|
61
|
+
!noLines && line !== 2 && "before:h-full",
|
|
60
62
|
)}
|
|
61
|
-
|
|
63
|
+
>
|
|
64
|
+
<svg viewBox="0 0 24 24" className="fill-tint-200 rtl:-scale-x-100">
|
|
65
|
+
{line === 0 && <path d="" />}
|
|
66
|
+
{!noLines && line === 1 && (
|
|
67
|
+
<path d="M7.45 26.1v-28.2h1.1v28.2h-1.1z" />
|
|
68
|
+
)}
|
|
69
|
+
{!noLines && line === 2 && (
|
|
70
|
+
<path d="M14.989 12.389l0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v14.1l0.161 0.389 0.389 0.161h6.6z" />
|
|
71
|
+
)}
|
|
72
|
+
{!noLines && line === 3 && (
|
|
73
|
+
<path d="M8.55 12.55h6.050l0.389-0.161 0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v28.2h1.1z" />
|
|
74
|
+
)}
|
|
75
|
+
</svg>
|
|
76
|
+
</div>
|
|
62
77
|
));
|
|
63
78
|
}, [noLines, node.lines]);
|
|
64
79
|
|
|
@@ -82,12 +97,21 @@ export const TreeNode = ({
|
|
|
82
97
|
<div
|
|
83
98
|
role="treeitem"
|
|
84
99
|
aria-selected={node.selected}
|
|
85
|
-
className=
|
|
100
|
+
className={classNames(
|
|
101
|
+
"flex flex-nowrap items-center",
|
|
102
|
+
node.className,
|
|
103
|
+
node.leaf ? leafClassName : nodeClassName,
|
|
104
|
+
)}
|
|
86
105
|
data-testid={node["data-testid"]}
|
|
87
106
|
data-test-value={node["data-test-value"]}
|
|
88
107
|
>
|
|
89
108
|
{filler}
|
|
90
|
-
{
|
|
109
|
+
{node.loading && (
|
|
110
|
+
<div className="flex-initial w-6">
|
|
111
|
+
<Icon icon={CoreIcons.spinner} className="animate-spin text-muted" />
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
{!node.leaf && !node.childFiltered && !node.loading && (
|
|
91
115
|
<div className="flex-initial w-6">
|
|
92
116
|
<Icon
|
|
93
117
|
rtlFlip
|
|
@@ -116,48 +140,66 @@ export const TreeNode = ({
|
|
|
116
140
|
/>
|
|
117
141
|
</div>
|
|
118
142
|
)}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
{!node.empty && (
|
|
144
|
+
<div
|
|
145
|
+
role="none"
|
|
146
|
+
className={classNames(
|
|
147
|
+
"group/tool data-[selected]:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none",
|
|
148
|
+
node.childSelected && "font-medium",
|
|
149
|
+
node.disabled && "text-muted opacity-65",
|
|
150
|
+
!node.disabled &&
|
|
151
|
+
(canSelect || onClick) &&
|
|
152
|
+
"hover:bg-primary-50 cursor-pointer",
|
|
153
|
+
)}
|
|
154
|
+
data-selected={node.selected}
|
|
155
|
+
onClick={
|
|
156
|
+
node.disabled
|
|
157
|
+
? undefined
|
|
158
|
+
: () => {
|
|
159
|
+
onClick?.(node.id, node.data);
|
|
160
|
+
canSelect
|
|
161
|
+
? onSelect(node.id)
|
|
162
|
+
: canCheck
|
|
163
|
+
? onChecked(node.id)
|
|
164
|
+
: onToggle(node.id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
{nodeIcon && (
|
|
169
|
+
<div className="flex-content w-6 self-center leading-none">
|
|
170
|
+
{isValidElement(nodeIcon) && nodeIcon}
|
|
171
|
+
{isString(nodeIcon) && (
|
|
172
|
+
<Icon
|
|
173
|
+
icon={nodeIcon}
|
|
174
|
+
bg={node.iconBg}
|
|
175
|
+
color={node.iconColor}
|
|
176
|
+
rtlFlip={node.rtlFlip}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
{node.childSelected && !node.open && (
|
|
182
|
+
<span className=" flex-content text-primary-500">•</span>
|
|
183
|
+
)}
|
|
184
|
+
<div role="none" className="flex-1 truncate">
|
|
185
|
+
{children}
|
|
143
186
|
</div>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
{!isEmpty(node.badge) && (
|
|
188
|
+
<Badge
|
|
189
|
+
{...badgeProps}
|
|
190
|
+
className={classNames(
|
|
191
|
+
"me-1 self-center flex-content bg-opacity-50",
|
|
192
|
+
badgeProps.className,
|
|
193
|
+
)}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
150
196
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
197
|
+
)}
|
|
198
|
+
{node.empty && (
|
|
199
|
+
<div className="text-muted text-sm select-none">
|
|
200
|
+
{t("tree.noItems")}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
161
203
|
</div>
|
|
162
204
|
);
|
|
163
205
|
};
|
package/src/tree/TreePanel.tsx
CHANGED
|
@@ -38,6 +38,47 @@ import { useTree } from "./useTree";
|
|
|
38
38
|
|
|
39
39
|
// TODO: implement keyboard handler for navigating tree
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* TreePanel component renders a virtualized tree structure with support for
|
|
43
|
+
* selection, expansion, and filtering. It allows for custom rendering of tree
|
|
44
|
+
* nodes and supports checkable nodes.
|
|
45
|
+
*
|
|
46
|
+
* @param {TreePanelProps<T>} props - The properties for the TreePanel component.
|
|
47
|
+
* @returns {JSX.Element} The rendered TreePanel component.
|
|
48
|
+
* @template T - The type of items in the tree, extending KeyValue.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```jsx
|
|
52
|
+
* <TreePanel
|
|
53
|
+
* items={items}
|
|
54
|
+
* selected={selectedItem}
|
|
55
|
+
* checked={checkedItems}
|
|
56
|
+
* checkable={true}
|
|
57
|
+
* renderer={(item) => <span>{item.name}</span>}
|
|
58
|
+
* searchable={true}
|
|
59
|
+
* selectable={true}
|
|
60
|
+
* matcher={(item, query) => item.name.toLowerCase().includes(query.toLowerCase())}
|
|
61
|
+
* filterPlaceholder="Search..."
|
|
62
|
+
* defaultLeafIcon={<Icon icon={CoreIcons.file} />}
|
|
63
|
+
* defaultNodeIcon={<Icon icon={CoreIcons.folder} />}
|
|
64
|
+
* expander="caret"
|
|
65
|
+
* onChecked={(checkedItems) => console.log(checkedItems)}
|
|
66
|
+
* onLoad={(id) => console.log(`Load children for ${id}`)}
|
|
67
|
+
* onQuery={(query) => console.log(`Query: ${query}`)}
|
|
68
|
+
* onSelect={(selectedItem) => console.log(selectedItem)}
|
|
69
|
+
* onClick={(item) => console.log(`Clicked on ${item.id}`)}
|
|
70
|
+
* sorter={(a, b) => a.label.localeCompare(b.label)}
|
|
71
|
+
* makeLabel={(item) => item.name}
|
|
72
|
+
* makeIcon={(item) => item.icon ? <Icon icon={item.icon} /> : null}
|
|
73
|
+
* defaultExpanded={["root"]}
|
|
74
|
+
* onExpandToggle={(id, expanded) => console.log(`Toggle expand for ${id}: ${expanded}`)}
|
|
75
|
+
* leafClassName="leaf-node"
|
|
76
|
+
* nodeClassName="tree-node"
|
|
77
|
+
* />
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @see {@link https://adarshpastakia.github.io/react-fabric/?path=/docs/data-treepanel--docs}
|
|
81
|
+
*/
|
|
41
82
|
export const TreePanel = <T extends KeyValue>({
|
|
42
83
|
ref,
|
|
43
84
|
items,
|
|
@@ -55,7 +96,16 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
55
96
|
expander = "box",
|
|
56
97
|
onChecked,
|
|
57
98
|
onLoad,
|
|
99
|
+
onQuery,
|
|
58
100
|
onSelect,
|
|
101
|
+
onClick,
|
|
102
|
+
sorter,
|
|
103
|
+
makeLabel,
|
|
104
|
+
makeIcon,
|
|
105
|
+
defaultExpanded,
|
|
106
|
+
onExpandToggle,
|
|
107
|
+
leafClassName,
|
|
108
|
+
nodeClassName,
|
|
59
109
|
...aria
|
|
60
110
|
}: TreePanelProps<T>) => {
|
|
61
111
|
const {
|
|
@@ -72,8 +122,13 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
72
122
|
checked,
|
|
73
123
|
selected,
|
|
74
124
|
matcher,
|
|
125
|
+
sorter,
|
|
126
|
+
onLoad,
|
|
127
|
+
onQuery,
|
|
75
128
|
onSelect,
|
|
76
129
|
onChecked,
|
|
130
|
+
defaultExpanded,
|
|
131
|
+
onExpandToggle,
|
|
77
132
|
});
|
|
78
133
|
|
|
79
134
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
@@ -99,7 +154,7 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
99
154
|
ref,
|
|
100
155
|
() => ({
|
|
101
156
|
open: (id: string) => expand(id),
|
|
102
|
-
select: (id: string) =>
|
|
157
|
+
select: (id: string) => select(id),
|
|
103
158
|
}),
|
|
104
159
|
[],
|
|
105
160
|
);
|
|
@@ -151,12 +206,18 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
151
206
|
checkable={checkable}
|
|
152
207
|
selectable={selectable}
|
|
153
208
|
defaultLeafIcon={defaultLeafIcon}
|
|
154
|
-
defaultNodeIcon={
|
|
209
|
+
defaultNodeIcon={
|
|
210
|
+
makeIcon?.(tree[index].data) ?? defaultNodeIcon
|
|
211
|
+
}
|
|
155
212
|
onToggle={toggleExpand}
|
|
156
213
|
onChecked={toggleCheck}
|
|
157
214
|
onSelect={select}
|
|
215
|
+
onClick={onClick}
|
|
216
|
+
leafClassName={leafClassName}
|
|
217
|
+
nodeClassName={nodeClassName}
|
|
158
218
|
>
|
|
159
|
-
{(tree[index].data && renderer?.(tree[index].data))
|
|
219
|
+
{(tree[index].data && renderer?.(tree[index].data)) ??
|
|
220
|
+
makeLabel?.(tree[index].data) ??
|
|
160
221
|
tree[index].label}
|
|
161
222
|
</TreeNode>
|
|
162
223
|
</div>
|
package/src/tree/types.ts
CHANGED
|
@@ -30,8 +30,9 @@ import {
|
|
|
30
30
|
type RefProp,
|
|
31
31
|
type TestProps,
|
|
32
32
|
} from "@react-fabric/core/dist/types/types";
|
|
33
|
+
import { type ElementType } from "react";
|
|
33
34
|
|
|
34
|
-
interface TreeBaseNode<T> extends IconProps, CssProp, TestProps {
|
|
35
|
+
interface TreeBaseNode<T> extends Omit<IconProps, "icon">, CssProp, TestProps {
|
|
35
36
|
id: string;
|
|
36
37
|
/**
|
|
37
38
|
* display label
|
|
@@ -54,20 +55,25 @@ interface TreeBaseNode<T> extends IconProps, CssProp, TestProps {
|
|
|
54
55
|
*/
|
|
55
56
|
disabled?: boolean;
|
|
56
57
|
data?: T;
|
|
58
|
+
|
|
59
|
+
icon?: ElementType<IconProps> | string;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export type TreeNodeType<T extends KeyValue = KeyValue> = TreeBaseNode<T> &
|
|
60
63
|
(
|
|
61
64
|
| { leaf: true; children?: never }
|
|
62
65
|
| {
|
|
63
|
-
leaf?:
|
|
66
|
+
leaf?: boolean;
|
|
64
67
|
children?: Array<TreeNodeType<T>>;
|
|
65
68
|
open?: boolean;
|
|
66
69
|
iconOpen?: string;
|
|
67
70
|
}
|
|
68
71
|
);
|
|
69
72
|
|
|
70
|
-
export interface InternalNode
|
|
73
|
+
export interface InternalNode
|
|
74
|
+
extends Omit<IconProps, "icon">,
|
|
75
|
+
CssProp,
|
|
76
|
+
TestProps {
|
|
71
77
|
id: string;
|
|
72
78
|
label: Elements<JSX.Element>;
|
|
73
79
|
queryable?: string;
|
|
@@ -84,6 +90,7 @@ export interface InternalNode extends IconProps, CssProp, TestProps {
|
|
|
84
90
|
loaded: boolean;
|
|
85
91
|
parent?: string;
|
|
86
92
|
empty?: boolean;
|
|
93
|
+
icon?: ElementType<IconProps> | string;
|
|
87
94
|
errored?: string;
|
|
88
95
|
selected?: true;
|
|
89
96
|
childSelected?: true;
|
|
@@ -101,9 +108,12 @@ export interface TreeNodeProps extends ChildProp {
|
|
|
101
108
|
noLines?: boolean;
|
|
102
109
|
selectable?: true | "leafOnly";
|
|
103
110
|
checkable?: true | "leafOnly";
|
|
111
|
+
leafClassName?: string;
|
|
112
|
+
nodeClassName?: string;
|
|
104
113
|
onToggle: (id: string) => void;
|
|
105
114
|
onSelect: (id: string) => void;
|
|
106
115
|
onChecked: (id: string) => void;
|
|
116
|
+
onClick?: (id: string, node: any) => void;
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
export interface TreeRef {
|
|
@@ -164,10 +174,18 @@ export interface TreePanelProps<T extends KeyValue = KeyValue>
|
|
|
164
174
|
| Promise<Array<TreeNodeType<T>> | undefined>
|
|
165
175
|
| Array<TreeNodeType<T>>
|
|
166
176
|
| undefined;
|
|
177
|
+
/**
|
|
178
|
+
* callback to load tree using search query
|
|
179
|
+
*/
|
|
180
|
+
onQuery?: (query: string) => void;
|
|
181
|
+
/**
|
|
182
|
+
* callback on click of tree node
|
|
183
|
+
*/
|
|
184
|
+
onClick?: (id: string, data: T) => void;
|
|
167
185
|
/**
|
|
168
186
|
* callback on selection of tree node
|
|
169
187
|
*/
|
|
170
|
-
onSelect?: (id: string) => void;
|
|
188
|
+
onSelect?: (id: string, data: T) => void;
|
|
171
189
|
/**
|
|
172
190
|
* callback on change of checked list
|
|
173
191
|
*/
|
|
@@ -180,6 +198,16 @@ export interface TreePanelProps<T extends KeyValue = KeyValue>
|
|
|
180
198
|
* item filter matcher
|
|
181
199
|
*/
|
|
182
200
|
matcher?: (data: T, query: string) => boolean;
|
|
201
|
+
|
|
202
|
+
sorter?: false | ((a: T, b: T) => number);
|
|
203
|
+
makeLabel?: (node: T) => string;
|
|
204
|
+
makeIcon?: (node: T) => string;
|
|
205
|
+
|
|
206
|
+
defaultExpanded?: string[];
|
|
207
|
+
onExpandToggle?: (id: string[]) => void;
|
|
208
|
+
|
|
209
|
+
leafClassName?: string;
|
|
210
|
+
nodeClassName?: string;
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
export const iconExpandAll =
|