@react-fabric/data 1.0.0-beta.6 → 1.0.0-beta.61

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.
Files changed (104) hide show
  1. package/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
  2. package/{dist/esm/tree/types.js → css/styles.css} +2 -7
  3. package/dist/cjs/checklist/CheckList.js +27 -4
  4. package/dist/cjs/histogram/Histogram.js +39 -9
  5. package/dist/cjs/hooks/useFilteredList.js +88 -10
  6. package/dist/cjs/hooks/usePagination.js +27 -4
  7. package/dist/cjs/hooks/useSelectableList.js +29 -0
  8. package/dist/cjs/i18n/ar.json.js +8 -1
  9. package/dist/cjs/i18n/en.json.js +8 -1
  10. package/dist/cjs/json/JsonViewer.js +68 -28
  11. package/dist/cjs/pagination/Pagination.js +14 -4
  12. package/dist/cjs/table/AddColumn.js +3 -3
  13. package/dist/cjs/table/BodyCell.js +14 -6
  14. package/dist/cjs/table/BodyRow.js +4 -4
  15. package/dist/cjs/table/CheckboxCell.js +2 -2
  16. package/dist/cjs/table/ColumnFilters.js +97 -0
  17. package/dist/cjs/table/HeaderCell.js +71 -43
  18. package/dist/cjs/table/Table.js +62 -13
  19. package/dist/cjs/table/types.js +29 -0
  20. package/dist/cjs/table/useResizer.js +4 -4
  21. package/dist/cjs/table/useVirtualTable.js +127 -19
  22. package/dist/cjs/tree/TreeNode.js +50 -32
  23. package/dist/cjs/tree/TreePanel.js +52 -19
  24. package/dist/cjs/tree/useTree.js +199 -38
  25. package/dist/cjs/tree/utils.js +39 -12
  26. package/dist/cjs/virtual/Gallery.js +97 -45
  27. package/dist/cjs/virtual/List.js +93 -39
  28. package/dist/cjs/virtual/Scroller.js +16 -8
  29. package/dist/esm/checklist/CheckList.js +28 -5
  30. package/dist/esm/histogram/Histogram.js +41 -11
  31. package/dist/esm/hooks/useFilteredList.js +89 -11
  32. package/dist/esm/hooks/usePagination.js +27 -4
  33. package/dist/esm/hooks/useSelectableList.js +29 -0
  34. package/dist/esm/i18n/ar.json.js +8 -2
  35. package/dist/esm/i18n/en.json.js +8 -2
  36. package/dist/esm/json/JsonViewer.js +69 -29
  37. package/dist/esm/pagination/Pagination.js +15 -5
  38. package/dist/esm/table/AddColumn.js +4 -4
  39. package/dist/esm/table/BodyCell.js +15 -7
  40. package/dist/esm/table/BodyRow.js +4 -4
  41. package/dist/esm/table/CheckboxCell.js +3 -3
  42. package/dist/esm/table/ColumnFilters.js +97 -0
  43. package/dist/esm/table/HeaderCell.js +72 -44
  44. package/dist/esm/table/Table.js +63 -14
  45. package/dist/esm/table/types.js +29 -0
  46. package/dist/esm/table/useResizer.js +4 -4
  47. package/dist/esm/table/useVirtualTable.js +129 -21
  48. package/dist/esm/tree/TreeNode.js +53 -35
  49. package/dist/esm/tree/TreePanel.js +54 -21
  50. package/dist/esm/tree/useTree.js +202 -41
  51. package/dist/esm/tree/utils.js +40 -13
  52. package/dist/esm/virtual/Gallery.js +97 -45
  53. package/dist/esm/virtual/List.js +93 -39
  54. package/dist/esm/virtual/Scroller.js +17 -9
  55. package/dist/types/checklist/CheckList.d.ts +22 -0
  56. package/dist/types/histogram/Histogram.d.ts +28 -1
  57. package/dist/types/hooks/useFilteredList.d.ts +66 -5
  58. package/dist/types/hooks/usePagination.d.ts +19 -0
  59. package/dist/types/hooks/useSelectableList.d.ts +29 -0
  60. package/dist/types/json/JsonViewer.d.ts +27 -2
  61. package/dist/types/pagination/Pagination.d.ts +10 -1
  62. package/dist/types/table/ColumnFilters.d.ts +9 -0
  63. package/dist/types/table/HeaderCell.d.ts +1 -1
  64. package/dist/types/table/Table.d.ts +14 -1
  65. package/dist/types/table/types.d.ts +46 -7
  66. package/dist/types/table/useVirtualTable.d.ts +23 -9
  67. package/dist/types/tree/TreeNode.d.ts +1 -1
  68. package/dist/types/tree/TreePanel.d.ts +42 -1
  69. package/dist/types/tree/types.d.ts +65 -21
  70. package/dist/types/tree/useTree.d.ts +9 -10
  71. package/dist/types/tree/utils.d.ts +10 -3
  72. package/dist/types/virtual/Gallery.d.ts +42 -3
  73. package/dist/types/virtual/List.d.ts +42 -3
  74. package/package.json +16 -14
  75. package/src/checklist/CheckList.tsx +39 -7
  76. package/src/histogram/Histogram.tsx +43 -19
  77. package/src/hooks/useFilteredList.tsx +109 -9
  78. package/src/hooks/usePagination.tsx +32 -7
  79. package/src/hooks/useSelectableList.ts +29 -0
  80. package/src/i18n/ar.json +5 -0
  81. package/src/i18n/en.json +5 -0
  82. package/src/json/JsonViewer.tsx +76 -37
  83. package/src/pagination/Pagination.tsx +15 -5
  84. package/src/table/AddColumn.tsx +4 -4
  85. package/src/table/BodyCell.tsx +24 -7
  86. package/src/table/BodyRow.tsx +7 -4
  87. package/src/table/CheckboxCell.tsx +5 -5
  88. package/src/table/ColumnFilters.tsx +105 -0
  89. package/src/table/HeaderCell.tsx +74 -21
  90. package/src/table/Table.tsx +107 -12
  91. package/src/table/types.ts +49 -10
  92. package/src/table/useResizer.ts +5 -3
  93. package/src/table/useVirtualTable.ts +158 -27
  94. package/src/tree/TreeNode.tsx +96 -62
  95. package/src/tree/TreePanel.tsx +88 -22
  96. package/src/tree/types.ts +73 -23
  97. package/src/tree/useTree.ts +220 -44
  98. package/src/tree/utils.ts +58 -20
  99. package/src/virtual/Gallery.tsx +118 -54
  100. package/src/virtual/List.tsx +99 -39
  101. package/src/virtual/Scroller.tsx +25 -9
  102. package/dist/cjs/histogram/Histogram.module.css +0 -1
  103. package/dist/cjs/tree/types.js +0 -30
  104. package/dist/esm/histogram/Histogram.module.css +0 -1
@@ -21,37 +21,59 @@
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 { useVirtualizer } from "@tanstack/react-virtual";
25
- import { useCallback, useReducer, useRef } from "react";
24
+ import { compareValues, debounce, 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
- expanded: Key[];
29
- checked: Key[];
39
+ interface DataState<T = KeyValue> {
40
+ data: T[];
41
+ items: T[];
42
+ grouped: KeyValue<T[]>;
43
+ groups: AnyObject[];
44
+ expanded: AnyObject[];
45
+ checked: AnyObject[];
30
46
  allChecked: 0 | 1 | 2;
31
47
  }
32
48
 
33
49
  type DataAction =
34
- | { type: "expand"; key: Key }
35
- | { type: "check"; key: Key }
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 }
36
55
  | { type: "checkall" };
37
56
 
38
- export const useVirtualTable = (
39
- data: KeyValue[],
40
- keyProperty?: Key,
41
- onCheckChanged?: (checked: Key[]) => void,
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
+ loading?: boolean;
64
+ total: number;
65
+ onLoadMore?: (startIndex: number) => void;
66
+ onCheckChanged?: (checked: AnyObject[]) => void;
67
+ },
42
68
  ) => {
43
69
  // TODO: refactor data into item list, apply grouping if groupColumn provided
44
70
  // TODO: when grouping maintain map of group with children, {___GROUP___:true, label, items, open}
45
71
 
46
- const scrollerRef = useRef<HTMLDivElement>(null);
47
- const virtualizer = useVirtualizer({
48
- count: data.length,
49
- getScrollElement: () => scrollerRef.current,
50
- estimateSize: () => 24,
51
- });
52
-
53
72
  const [state, dispatch] = useReducer(
54
- (state: DataState, action: DataAction) => {
73
+ (state: DataState<T>, action: DataAction) => {
74
+ if (action.type === "updateState") {
75
+ state.checked = action.checked;
76
+ }
55
77
  if (action.type === "expand") {
56
78
  if (state.expanded.includes(action.key))
57
79
  state.expanded.splice(state.expanded.indexOf(action.key), 1);
@@ -61,51 +83,160 @@ export const useVirtualTable = (
61
83
  if (state.checked.includes(action.key))
62
84
  state.checked.splice(state.checked.indexOf(action.key), 1);
63
85
  else state.checked.push(action.key);
64
- onCheckChanged?.(state.checked);
86
+ options.onCheckChanged?.(state.checked);
65
87
  }
66
88
  if (action.type === "checkall") {
67
89
  if (state.allChecked !== 0) {
68
90
  state.checked = [];
69
- } else if (keyProperty) {
70
- state.checked = data.map((item) => item[keyProperty]);
91
+ } else if (options.keyProperty) {
92
+ state.checked = state.data.map((item) => item[options.keyProperty]);
93
+ }
94
+ options.onCheckChanged?.(state.checked);
95
+ }
96
+ const checkList = state.data.filter((hit) =>
97
+ state.checked.includes(hit[options.keyProperty]),
98
+ );
99
+ if (action.type === "groupExpand") {
100
+ const group = state.groups.find((group) => group.key === action.key);
101
+ if (group) {
102
+ group.open = !group.open;
103
+ state.groups = state.groups.slice();
104
+ state.items = state.groups
105
+ .map((group) => [
106
+ group,
107
+ group.open ? state.grouped[group.key] : undefined,
108
+ ])
109
+ .flat(2)
110
+ .filter(Boolean) as any;
111
+ }
112
+ }
113
+ if (action.type === "reset") {
114
+ state.data = action.data;
115
+ if (action.key) {
116
+ state.grouped = groupBy<T>(state.data, action.key);
117
+ state.groups = Object.keys(state.grouped)
118
+ .sort(compareValues("asc"))
119
+ .map((group) => ({
120
+ __GROUP__: true,
121
+ open: true,
122
+ key: group,
123
+ items: state.grouped[group],
124
+ itemCount: state.grouped[group].length,
125
+ }));
126
+ state.items = state.groups
127
+ .map((group) => [group, ...state.grouped[group.key]])
128
+ .flat() as any;
129
+ } else {
130
+ state.items = state.data;
71
131
  }
72
- onCheckChanged?.(state.checked);
73
132
  }
74
133
  return {
75
134
  ...state,
76
135
  allChecked:
77
- state.checked.length === data.length
136
+ checkList.length === state.data.length
78
137
  ? 1
79
138
  : state.checked.length
80
139
  ? 2
81
140
  : 0,
82
- } as DataState;
141
+ } as DataState<T>;
83
142
  },
84
143
  {
144
+ data,
145
+ items: [],
146
+ grouped: {},
147
+ groups: [],
85
148
  checked: [],
86
149
  expanded: [],
87
150
  allChecked: 0,
88
151
  },
89
152
  );
90
153
 
154
+ const activeStickyIndexRef = useRef(0);
155
+ const stickyIndexes = useMemo<number[]>(
156
+ () =>
157
+ state.items
158
+ .map((hit, idx) => ("__GROUP__" in hit ? idx : undefined))
159
+ .filter(Boolean) as number[],
160
+ [state.items],
161
+ );
162
+ const isActiveSticky = (index: number) =>
163
+ activeStickyIndexRef.current === index;
164
+
165
+ const [hasActiveSticky, setHasActiveSticky] = useState(false);
166
+ const scrollerRef = useRef<HTMLDivElement>(null);
167
+ const virtualizer = useVirtualizer({
168
+ count: state.items.length,
169
+ getScrollElement: () => scrollerRef.current,
170
+ estimateSize: () => 32,
171
+ rangeExtractor: useCallback(
172
+ (range: Range) => {
173
+ activeStickyIndexRef.current =
174
+ [...stickyIndexes]
175
+ .reverse()
176
+ .find((index) => range.startIndex > index) ?? 0;
177
+
178
+ setHasActiveSticky(range.startIndex !== activeStickyIndexRef.current);
179
+
180
+ const next = new Set([
181
+ activeStickyIndexRef.current,
182
+ ...defaultRangeExtractor(range),
183
+ ]);
184
+
185
+ return [...next];
186
+ },
187
+ [stickyIndexes],
188
+ ),
189
+ });
190
+
91
191
  const virtualItems = virtualizer.getVirtualItems();
92
192
 
93
193
  const getData = useCallback(
94
194
  (index: number) => {
95
- return data[index];
195
+ return state.items[index];
96
196
  },
97
- [data],
197
+ [state.items],
98
198
  );
99
199
 
200
+ useEffect(() => {
201
+ dispatch({ type: "reset", key: options.groupProperty, data });
202
+ }, [data, options.groupProperty]);
203
+
204
+ useEffect(() => {
205
+ dispatch({ type: "updateState", checked: options.checked ?? [] });
206
+ }, [options.checked]);
207
+
208
+ useEffect(() => {
209
+ if (options.onLoadMore) {
210
+ const firstItem = virtualItems[0]?.index;
211
+ const lastItem = virtualItems?.slice(-1).pop()?.index;
212
+ const canLoadPrevPage =
213
+ firstItem !== undefined && state.items[firstItem] === null;
214
+ const canLoadNextPage =
215
+ lastItem !== undefined &&
216
+ lastItem === state.items.length - 1 &&
217
+ state.items.length < options.total;
218
+ const db = debounce(options.onLoadMore);
219
+ if (canLoadPrevPage) {
220
+ db(firstItem);
221
+ } else if (!options.loading && canLoadNextPage) {
222
+ db(lastItem + 1);
223
+ }
224
+ return () => db.cancel();
225
+ }
226
+ }, [options.onLoadMore, virtualItems]);
227
+
100
228
  return {
101
229
  scrollerRef,
102
230
  items: virtualItems,
103
231
  checkState: state,
104
232
  getData,
105
233
  virtualizer,
234
+ hasActiveSticky,
235
+ isActiveSticky,
106
236
  top: virtualizer.range?.startIndex,
107
237
  totalSize: virtualizer.getTotalSize,
108
238
  measureElement: virtualizer.measureElement,
239
+ toggleGroupExpand: (key: Key) => dispatch({ type: "groupExpand", key }),
109
240
  toggleExpand: (key: Key) => dispatch({ type: "expand", key }),
110
241
  toggleChecked: (key: Key) => dispatch({ type: "check", key }),
111
242
  toggleAllChecked: () => dispatch({ type: "checkall" }),
@@ -21,9 +21,11 @@
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 { Badge, CoreIcons, getBadgeProps, Icon } from "@react-fabric/core";
24
+ import { Badge, getBadgeProps, getIconProps, Icon } from "@react-fabric/core";
25
+ import { isEmpty, isString, isTrue } 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,40 +37,53 @@ 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(noLines ? 1 : 0).map((line, idx) => (
48
- <svg
53
+ return node.lines.slice(1).map((line, idx) => (
54
+ <div
49
55
  key={idx}
50
- viewBox="0 0 24 24"
51
- className="flex-initial w-6 fill-tint-200"
52
- >
53
- {line === 0 && <path d="" />}
54
- {!noLines && line === 1 && <path d="M7.45 26.1v-28.2h1.1v28.2h-1.1z" />}
55
- {!noLines && line === 2 && (
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-0.5 before:scale-x-[.625]",
61
+ !noLines && line !== 2 && "before:h-full",
60
62
  )}
61
- </svg>
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
 
65
80
  const nodeIcon = useMemo(() => {
66
81
  if (node.icon) return node.icon;
67
- return node.leaf ? defaultLeafIcon : defaultNodeIcon;
82
+ return { icon: node.leaf ? defaultLeafIcon : defaultNodeIcon };
68
83
  }, [defaultLeafIcon, defaultNodeIcon, node.icon]);
69
84
 
70
85
  const canCheck = useMemo(() => {
71
- return checkable === true || (checkable === "leafOnly" && node.leaf);
86
+ return isTrue(checkable) || (checkable === "leafOnly" && node.leaf);
72
87
  }, [node.leaf, checkable]);
73
88
 
74
89
  const canSelect = useMemo(() => {
@@ -82,12 +97,18 @@ export const TreeNode = ({
82
97
  <div
83
98
  role="treeitem"
84
99
  aria-selected={node.selected}
85
- className="flex flex-nowrap"
100
+ className={classNames("flex flex-nowrap items-center")}
86
101
  data-testid={node["data-testid"]}
87
102
  data-test-value={node["data-test-value"]}
103
+ data-id={node.id}
88
104
  >
89
105
  {filler}
90
- {!node.leaf && !node.childFiltered && (
106
+ {node.loading && (
107
+ <div className="flex-initial w-6">
108
+ <Icon icon="icon-[svg-spinners--eclipse]" className="text-muted" />
109
+ </div>
110
+ )}
111
+ {!node.leaf && !node.childFiltered && !node.loading && (
91
112
  <div className="flex-initial w-6">
92
113
  <Icon
93
114
  rtlFlip
@@ -103,10 +124,10 @@ export const TreeNode = ({
103
124
  onClick={() => !node.disabled && onChecked(node.id)}
104
125
  icon={
105
126
  node.checked === 2
106
- ? CoreIcons.checkboxInt
127
+ ? "icon-[mdi--checkbox-intermediate]"
107
128
  : node.checked === 1
108
- ? CoreIcons.checkboxOn
109
- : CoreIcons.checkboxOff
129
+ ? "icon-[mdi--checkbox-marked]"
130
+ : "icon-[mdi--checkbox-blank-outline]"
110
131
  }
111
132
  className={classNames(
112
133
  node.disabled
@@ -116,48 +137,61 @@ export const TreeNode = ({
116
137
  />
117
138
  </div>
118
139
  )}
119
- <div
120
- role="none"
121
- className={classNames(
122
- "group data-[selected]:bg-primary-100 flex flex-nowrap flex-1 items-center overflow-hidden select-none",
123
- node.childSelected && "font-medium",
124
- canSelect && "hover:bg-primary-50 cursor-pointer",
125
- )}
126
- data-selected={node.selected}
127
- onClick={() =>
128
- canSelect
129
- ? onSelect(node.id)
130
- : canCheck
131
- ? onChecked(node.id)
132
- : onToggle(node.id)
133
- }
134
- >
135
- {nodeIcon && (
136
- <div className="flex-content w-6">
137
- <Icon
138
- icon={nodeIcon}
139
- bg={node.iconBg}
140
- color={node.iconColor}
141
- rtlFlip={node.rtlFlip}
142
- />
140
+ {!node.empty && (
141
+ <div
142
+ role="none"
143
+ className={classNames(
144
+ "group/tool data-selected:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none",
145
+ node.childSelected && "font-medium",
146
+ node.disabled && "text-muted opacity-65",
147
+ node.className,
148
+ node.leaf ? leafClassName : nodeClassName,
149
+ !node.disabled &&
150
+ (canSelect || onClick) &&
151
+ "hover:bg-primary-50 cursor-pointer",
152
+ )}
153
+ data-selected={node.selected}
154
+ onClick={
155
+ node.disabled
156
+ ? undefined
157
+ : (e) => {
158
+ onClick?.(node.id, node.data);
159
+ canSelect
160
+ ? onSelect(node.id, e.shiftKey)
161
+ : canCheck
162
+ ? onChecked(node.id)
163
+ : onToggle(node.id);
164
+ }
165
+ }
166
+ >
167
+ {nodeIcon && (
168
+ <div className="flex-content max-w-6 me-1 self-center leading-none">
169
+ {isValidElement(nodeIcon) && nodeIcon}
170
+ {isString(nodeIcon) && <Icon {...getIconProps(nodeIcon)} />}
171
+ </div>
172
+ )}
173
+ {node.childSelected && !node.open && (
174
+ <span className=" flex-content text-primary-500">•</span>
175
+ )}
176
+ <div role="none" className="flex-1 truncate">
177
+ {children}
143
178
  </div>
144
- )}
145
- {node.childSelected && !node.open && (
146
- <span className=" flex-content text-primary-500">•</span>
147
- )}
148
- <div role="none" className="flex-1 truncate">
149
- {children}
179
+ {!isEmpty(node.badge) && (
180
+ <Badge
181
+ {...badgeProps}
182
+ className={classNames(
183
+ "me-1 self-center flex-content bg-opacity-50",
184
+ badgeProps.className,
185
+ )}
186
+ />
187
+ )}
150
188
  </div>
151
- {node.badge && (
152
- <Badge
153
- {...badgeProps}
154
- className={classNames(
155
- "me-1 self-center flex-content bg-opacity-50",
156
- badgeProps.className,
157
- )}
158
- />
159
- )}
160
- </div>
189
+ )}
190
+ {node.empty && (
191
+ <div className="text-muted text-sm select-none">
192
+ {t("tree.noItems")}
193
+ </div>
194
+ )}
161
195
  </div>
162
196
  );
163
197
  };
@@ -21,23 +21,57 @@
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
- Button,
26
- ButtonGroup,
27
- CoreIcons,
28
- Header,
29
- Icon,
30
- Section,
31
- } from "@react-fabric/core";
24
+ import { Button, ButtonGroup, Header, Icon, Section } from "@react-fabric/core";
32
25
  import { Search } from "@react-fabric/form";
33
26
  import { useVirtualizer } from "@tanstack/react-virtual";
34
- import { useImperativeHandle, useMemo, useRef } from "react";
27
+ import { useEffect, useImperativeHandle, useMemo, useRef } from "react";
35
28
  import { TreeNode } from "./TreeNode";
36
- import { iconCollapseAll, iconExpandAll, type TreePanelProps } from "./types";
29
+ import { type TreePanelProps } from "./types";
37
30
  import { useTree } from "./useTree";
38
31
 
39
32
  // TODO: implement keyboard handler for navigating tree
40
33
 
34
+ /**
35
+ * TreePanel component renders a virtualized tree structure with support for
36
+ * selection, expansion, and filtering. It allows for custom rendering of tree
37
+ * nodes and supports checkable nodes.
38
+ *
39
+ * @param {TreePanelProps<T>} props - The properties for the TreePanel component.
40
+ * @returns {JSX.Element} The rendered TreePanel component.
41
+ * @template T - The type of items in the tree, extending KeyValue.
42
+ *
43
+ * @example
44
+ * ```jsx
45
+ * <TreePanel
46
+ * items={items}
47
+ * selected={selectedItem}
48
+ * checked={checkedItems}
49
+ * checkable={true}
50
+ * renderer={(item) => <span>{item.name}</span>}
51
+ * searchable={true}
52
+ * selectable={true}
53
+ * matcher={(item, query) => item.name.toLowerCase().includes(query.toLowerCase())}
54
+ * filterPlaceholder="Search..."
55
+ * defaultLeafIcon={<Icon icon="icon-[mdi--file]" />}
56
+ * defaultNodeIcon={<Icon icon="icon-[mdi--folder]" />}
57
+ * expander="caret"
58
+ * onChecked={(checkedItems) => console.log(checkedItems)}
59
+ * onLoad={(id) => console.log(`Load children for ${id}`)}
60
+ * onQuery={(query) => console.log(`Query: ${query}`)}
61
+ * onSelect={(selectedItem) => console.log(selectedItem)}
62
+ * onClick={(item) => console.log(`Clicked on ${item.id}`)}
63
+ * sorter={(a, b) => a.label.localeCompare(b.label)}
64
+ * makeLabel={(item) => item.name}
65
+ * makeIcon={(item) => item.icon ? <Icon icon={item.icon} /> : null}
66
+ * defaultExpanded={["root"]}
67
+ * onExpandToggle={(id, expanded) => console.log(`Toggle expand for ${id}: ${expanded}`)}
68
+ * leafClassName="leaf-node"
69
+ * nodeClassName="tree-node"
70
+ * />
71
+ * ```
72
+ *
73
+ * @see {@link https://adarshpastakia.github.io/react-fabric/?path=/docs/data-treepanel--docs}
74
+ */
41
75
  export const TreePanel = <T extends KeyValue>({
42
76
  ref,
43
77
  items,
@@ -45,9 +79,9 @@ export const TreePanel = <T extends KeyValue>({
45
79
  checked,
46
80
  checkable,
47
81
  noLines,
48
- renderer,
49
82
  searchable,
50
83
  selectable,
84
+ multiple,
51
85
  matcher,
52
86
  filterPlaceholder,
53
87
  defaultLeafIcon,
@@ -55,15 +89,27 @@ export const TreePanel = <T extends KeyValue>({
55
89
  expander = "box",
56
90
  onChecked,
57
91
  onLoad,
92
+ onQuery,
58
93
  onSelect,
94
+ onClick,
95
+ sorter,
96
+ makeLabel,
97
+ makeIcon,
98
+ defaultExpanded,
99
+ onExpandToggle,
100
+ leafClassName,
101
+ nodeClassName,
59
102
  ...aria
60
103
  }: TreePanelProps<T>) => {
61
104
  const {
62
105
  tree,
106
+ query,
107
+ selected: currentSelected,
63
108
  toggleExpand,
64
109
  toggleCheck,
65
110
  expandAll,
66
111
  collapseAll,
112
+ expandAndLoad,
67
113
  select,
68
114
  expand,
69
115
  onFilter,
@@ -71,10 +117,16 @@ export const TreePanel = <T extends KeyValue>({
71
117
  items,
72
118
  checked,
73
119
  selected,
120
+ multiple,
74
121
  matcher,
122
+ sorter,
123
+ onLoad,
124
+ onQuery,
75
125
  onSelect,
76
126
  onChecked,
77
- });
127
+ defaultExpanded,
128
+ onExpandToggle,
129
+ } as any);
78
130
 
79
131
  const scrollerRef = useRef<HTMLDivElement>(null);
80
132
  const virtualizer = useVirtualizer({
@@ -87,23 +139,29 @@ export const TreePanel = <T extends KeyValue>({
87
139
 
88
140
  const expanders = useMemo<[string, string]>(() => {
89
141
  if (expander === "caret")
90
- return [CoreIcons.caretRight, CoreIcons.caretDown];
142
+ return ["icon-[mdi--menu-right]", "icon-[mdi--menu-down]"];
91
143
  if (expander === "chevron")
92
- return [CoreIcons.chevronRight, CoreIcons.chevronDown];
144
+ return ["icon-[mdi--chevron-right]", "icon-[mdi--chevron-down]"];
93
145
  if (expander === "folder")
94
- return [CoreIcons.folderClosed, CoreIcons.folderOpen];
95
- return [CoreIcons.expand, CoreIcons.collapse];
146
+ return ["icon-[mdi--folder-outline]", "icon-[mdi--folder-open-outline]"];
147
+ return ["icon-[mdi--plus-box-outline]", "icon-[mdi--minus-box-outline]"];
96
148
  }, [expander]);
97
149
 
98
150
  useImperativeHandle(
99
151
  ref,
100
152
  () => ({
101
153
  open: (id: string) => expand(id),
102
- select: (id: string) => expand(id),
154
+ openAndLoad: (list: string[]) => expandAndLoad(list),
155
+ select: (id: string) => select(id),
103
156
  }),
104
157
  [],
105
158
  );
106
159
 
160
+ useEffect(() => {
161
+ const index = tree.findIndex((n) => n.id === currentSelected);
162
+ virtualizer.scrollToIndex(index, { align: "auto" });
163
+ }, [currentSelected]);
164
+
107
165
  return (
108
166
  <Section>
109
167
  <Header
@@ -112,24 +170,27 @@ export const TreePanel = <T extends KeyValue>({
112
170
  >
113
171
  {searchable && (
114
172
  <Search
173
+ value={query}
115
174
  onSearch={onFilter}
116
175
  allowClear
117
176
  placeholder={filterPlaceholder}
118
177
  {...{ noBorder: true }}
119
- decorateStart={<Icon icon={CoreIcons.filter} className="p-1" />}
178
+ decorateStart={
179
+ <Icon icon="icon-[mdi--filter-variant]" className="p-1" />
180
+ }
120
181
  />
121
182
  )}
122
183
  <ButtonGroup variant="link" className="m-0">
123
184
  <Button
124
185
  size="sm"
125
186
  aria-label="expand all"
126
- icon={iconExpandAll}
187
+ icon="icon-[mdi--arrow-expand-vertical]"
127
188
  onClick={expandAll}
128
189
  />
129
190
  <Button
130
191
  size="sm"
131
192
  aria-label="collapse all"
132
- icon={iconCollapseAll}
193
+ icon="icon-[mdi--arrow-collapse-vertical]"
133
194
  onClick={collapseAll}
134
195
  />
135
196
  </ButtonGroup>
@@ -151,12 +212,17 @@ export const TreePanel = <T extends KeyValue>({
151
212
  checkable={checkable}
152
213
  selectable={selectable}
153
214
  defaultLeafIcon={defaultLeafIcon}
154
- defaultNodeIcon={defaultNodeIcon}
215
+ defaultNodeIcon={
216
+ makeIcon?.(tree[index].data) ?? defaultNodeIcon
217
+ }
155
218
  onToggle={toggleExpand}
156
219
  onChecked={toggleCheck}
157
220
  onSelect={select}
221
+ onClick={onClick}
222
+ leafClassName={leafClassName}
223
+ nodeClassName={nodeClassName}
158
224
  >
159
- {(tree[index].data && renderer?.(tree[index].data)) ||
225
+ {(tree[index].data && makeLabel?.(tree[index].data)) ??
160
226
  tree[index].label}
161
227
  </TreeNode>
162
228
  </div>