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

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 (103) hide show
  1. package/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
  2. package/css/styles.css +24 -0
  3. package/dist/cjs/checklist/CheckList.js +23 -0
  4. package/dist/cjs/histogram/Histogram.js +37 -7
  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 +67 -27
  11. package/dist/cjs/pagination/Pagination.js +10 -0
  12. package/dist/cjs/table/AddColumn.js +1 -1
  13. package/dist/cjs/table/BodyCell.js +5 -4
  14. package/dist/cjs/table/BodyRow.js +2 -2
  15. package/dist/cjs/table/CheckboxCell.js +1 -1
  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 +58 -12
  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 +48 -30
  23. package/dist/cjs/tree/TreePanel.js +45 -11
  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 +23 -0
  30. package/dist/esm/histogram/Histogram.js +38 -8
  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 +68 -28
  37. package/dist/esm/pagination/Pagination.js +10 -0
  38. package/dist/esm/table/AddColumn.js +1 -1
  39. package/dist/esm/table/BodyCell.js +5 -4
  40. package/dist/esm/table/BodyRow.js +2 -2
  41. package/dist/esm/table/CheckboxCell.js +1 -1
  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 +59 -13
  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 +51 -33
  49. package/dist/esm/tree/TreePanel.js +46 -12
  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 +16 -8
  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 +44 -5
  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 -19
  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 +22 -0
  76. package/src/histogram/Histogram.tsx +40 -10
  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 +71 -35
  83. package/src/pagination/Pagination.tsx +10 -0
  84. package/src/table/AddColumn.tsx +1 -1
  85. package/src/table/BodyCell.tsx +6 -4
  86. package/src/table/BodyRow.tsx +3 -2
  87. package/src/table/CheckboxCell.tsx +1 -1
  88. package/src/table/ColumnFilters.tsx +105 -0
  89. package/src/table/HeaderCell.tsx +72 -18
  90. package/src/table/Table.tsx +99 -11
  91. package/src/table/types.ts +46 -8
  92. package/src/table/useResizer.ts +5 -3
  93. package/src/table/useVirtualTable.ts +158 -27
  94. package/src/tree/TreeNode.tsx +98 -58
  95. package/src/tree/TreePanel.tsx +77 -6
  96. package/src/tree/types.ts +73 -18
  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 +16 -8
  102. package/dist/cjs/histogram/Histogram.module.css +0 -1
  103. package/dist/esm/histogram/Histogram.module.css +0 -1
@@ -21,8 +21,15 @@
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 { EmptyContent, Loading, useDebounce } from "@react-fabric/core";
24
+ import {
25
+ CoreIcons,
26
+ EmptyContent,
27
+ Icon,
28
+ Loading,
29
+ useDebounce,
30
+ } from "@react-fabric/core";
25
31
  import { debounce } from "@react-fabric/utilities";
32
+ import classNames from "classnames";
26
33
  import {
27
34
  useEffect,
28
35
  useImperativeHandle,
@@ -41,23 +48,42 @@ import { type TableProps } from "./types";
41
48
  import { useTableColumns } from "./useTableColumns";
42
49
  import { useVirtualTable } from "./useVirtualTable";
43
50
 
51
+ /**
52
+ * Table component for displaying data in a tabular format with features like sorting, filtering, and virtual scrolling.
53
+ * It supports expandable rows, checkable rows, and customizable columns.
54
+ * The component is designed to handle large datasets efficiently by rendering only the visible rows.
55
+ * It also provides a way to manage column visibility and allows for custom rendering of group headers.
56
+ * Uses TanStack's `useVirtual` for virtualization and React's `useMemo` for performance optimization.
57
+ *
58
+ * @param {TableProps<T>} props - The properties for the Table component.
59
+ * @returns {JSX.Element} The rendered Table component.
60
+ *
61
+ * @see {@link https://adarshpastakia.github.io/react-fabric/?path=/story/data-table--table}
62
+ * @see {@link https://tanstack.com/table/v8/docs/api/virtual}
63
+ */
44
64
  export const Table = <T extends KeyValue = KeyValue>({
45
65
  ref,
46
66
  data,
47
67
  columns,
48
- keyProperty,
68
+ keyProperty = "id",
69
+ groupProperty,
49
70
  checkableRows,
50
71
  initialScroll,
51
72
  hideableColumns,
52
73
  sort,
53
74
  loading,
75
+ checked,
54
76
  emptyDisplay,
77
+ groupRenderer,
55
78
  canExpand,
56
79
  children,
57
80
  onSort,
81
+ onFilter,
58
82
  onScroll,
59
83
  onRowClick,
60
84
  onCheckedChanged,
85
+ total = 0,
86
+ onLoadMore,
61
87
  }: TableProps<T>) => {
62
88
  const { t } = useTranslation("data");
63
89
  const fireCheckChanged = useDebounce(
@@ -72,13 +98,24 @@ export const Table = <T extends KeyValue = KeyValue>({
72
98
  top,
73
99
  checkState,
74
100
  virtualizer,
101
+ hasActiveSticky,
75
102
  toggleExpand,
76
103
  toggleChecked,
77
104
  toggleAllChecked,
105
+ toggleGroupExpand,
78
106
  getData,
79
107
  totalSize,
108
+ isActiveSticky,
80
109
  measureElement,
81
- } = useVirtualTable(data, keyProperty, fireCheckChanged);
110
+ } = useVirtualTable(data, {
111
+ checked,
112
+ keyProperty,
113
+ groupProperty,
114
+ total,
115
+ loading,
116
+ onLoadMore,
117
+ onCheckChanged: fireCheckChanged,
118
+ });
82
119
  const refBody = useRef<HTMLDivElement>(null);
83
120
 
84
121
  const wrapperStart = useMemo(
@@ -142,9 +179,13 @@ export const Table = <T extends KeyValue = KeyValue>({
142
179
  style={{
143
180
  gridTemplate: `"head" auto "loader" auto "content" 1fr "foot" auto/1fr`,
144
181
  }}
182
+ data-ref="tableScroller"
145
183
  ref={scrollerRef}
146
184
  >
147
- <div className="bg-base font-medium sticky top-0 area-head border-b select-none flex flex-nowrap z-2">
185
+ <div
186
+ data-ref="tableHeader"
187
+ className="bg-base font-medium sticky top-0 h-8 area-head border-b select-none flex flex-nowrap z-3"
188
+ >
148
189
  <div className={wrapperStart}>
149
190
  {canExpand && <div className="w-6" />}
150
191
  {checkableRows && (
@@ -154,15 +195,33 @@ export const Table = <T extends KeyValue = KeyValue>({
154
195
  />
155
196
  )}
156
197
  {state.start?.map((col, idx) => (
157
- <HeaderCell key={idx} {...col} sort={sort} onSort={onSort} />
198
+ <HeaderCell
199
+ key={idx}
200
+ {...col}
201
+ sort={sort}
202
+ onSort={onSort}
203
+ onFilter={onFilter}
204
+ />
158
205
  ))}
159
206
  </div>
160
207
  {state.cols?.map((col, idx) => (
161
- <HeaderCell key={idx} {...col} sort={sort} onSort={onSort} />
208
+ <HeaderCell
209
+ key={idx}
210
+ {...col}
211
+ sort={sort}
212
+ onSort={onSort}
213
+ onFilter={onFilter}
214
+ />
162
215
  ))}
163
216
  <div className={wrapperEnd}>
164
217
  {state.end?.map((col, idx) => (
165
- <HeaderCell key={idx} {...col} sort={sort} onSort={onSort} />
218
+ <HeaderCell
219
+ key={idx}
220
+ {...col}
221
+ sort={sort}
222
+ onSort={onSort}
223
+ onFilter={onFilter}
224
+ />
166
225
  ))}
167
226
 
168
227
  {hideableColumns && (
@@ -176,16 +235,45 @@ export const Table = <T extends KeyValue = KeyValue>({
176
235
  {loading && <Loading />}
177
236
  {data.length > 0 && (
178
237
  <div
179
- className="area-content flex flex-col flex-nowrap"
238
+ className="area-content flex flex-col flex-nowrap min-h-full"
180
239
  style={{ height: totalSize() }}
181
240
  ref={refBody}
182
241
  >
183
- <div style={{ height: items[0]?.start }} />
242
+ <div
243
+ style={{
244
+ height: items[hasActiveSticky ? 1 : 0]?.start,
245
+ }}
246
+ />
184
247
  {items.map(({ index, key }) => {
185
248
  const data = getData(index);
186
- return (
249
+ return "__GROUP__" in data ? (
250
+ <div
251
+ key={key}
252
+ ref={measureElement}
253
+ data-index={index}
254
+ className={classNames(
255
+ "px-2 py-1 text-md bg-base border-y border-tint-100 cursor-pointer flex items-center gap-1 z-2",
256
+ isActiveSticky(index) && "sticky top-8",
257
+ )}
258
+ role="none"
259
+ onClick={() => toggleGroupExpand(data.key)}
260
+ >
261
+ <Icon
262
+ rtlFlip
263
+ icon={
264
+ data.open ? CoreIcons.chevronDown : CoreIcons.chevronRight
265
+ }
266
+ />
267
+ {groupRenderer?.(data) ?? (
268
+ <div className="flex gap-1 items-center">
269
+ <span>{data.key}</span>
270
+ <span className="text-sm">({data.itemCount})</span>
271
+ </div>
272
+ )}
273
+ </div>
274
+ ) : (
187
275
  <BodyRow
188
- rowKey={key}
276
+ rowKey={key.toString()}
189
277
  index={index}
190
278
  checkableRows={checkableRows}
191
279
  checked={
@@ -21,10 +21,10 @@
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 { type IconProps } from "@react-fabric/core/dist/types/components/icon/Icon";
24
25
  import {
25
26
  type CssProp,
26
27
  type Elements,
27
- type IconProps,
28
28
  type RefProp,
29
29
  type TestProps,
30
30
  } from "@react-fabric/core/dist/types/types";
@@ -34,22 +34,51 @@ export const COL_MIN_WIDTH = "3rem";
34
34
  export const COL_MAX_WIDTH = "64rem";
35
35
  export const COL_DEFAULT_WIDTH = "12rem";
36
36
 
37
- export interface TableColumn<T = KeyValue>
38
- extends CssProp,
39
- IconProps,
40
- TestProps {
37
+ /**
38
+ * TableColumn interface defines the structure of a column in a table.
39
+ * It includes properties for column identification, display options, alignment,
40
+ * visibility, resizing, sorting, filtering, and rendering.
41
+ *
42
+ * @template T - The type of data the column will represent, defaulting to KeyValue.
43
+ * @property {keyof T | string} id - Unique identifier for the column.
44
+ * @property {"string" | "number" | "boolean" | "datetime" | "date" | "time" | "unknown"} [dataType] - Data type of the column values.
45
+ * @property {string} [label] - Display label for the column header.
46
+ * @property {string} [tooltip] - Tooltip text for the column header.
47
+ * @property {string} [format] - Format string for displaying the column values.
48
+ * @property {"start" | "center" | "end"} [align] - Text alignment within the column.
49
+ * @property {"start" | "end"} [locked] - Indicates if the column is locked at the start or end.
50
+ * @property {boolean} [hidden] - Whether the column is hidden.
51
+ * @property {boolean} [truncate] - Whether to truncate the text in the column.
52
+ * @property {boolean} [hideable] - Whether the column can be hidden by the user.
53
+ * @property {boolean} [resizable] - Whether the column can be resized by the user.
54
+ * @property {boolean} [sortable] - Whether the column can be sorted.
55
+ * @property {boolean} [filterable] - Whether the column can be filtered.
56
+ * @property {AnyObject} [filter] - Filter criteria for the column.
57
+ * @property {ReactElement[]} [actions] - Array of action elements to be displayed in the column.
58
+ * @property {AnyObject[]} [filterOptions] - Options for filtering the column.
59
+ * @property {string | number} [width] - Width of the column.
60
+ * @property {string | number} [minWidth] - Minimum width of the column.
61
+ * @property {string | number} [maxWidth] - Maximum width of the column.
62
+ * @property {KeyValue} [valueMap] - Mapping of values for the column.
63
+ * @property {(value: AnyObject, data: T, index: number) => Elements<AnyObject>} [renderer] - Custom renderer function for the column values.
64
+ */
65
+ export interface TableColumn<T = KeyValue> extends CssProp, TestProps {
41
66
  id: keyof T | string;
67
+ field?: string;
42
68
  label?: string;
43
69
  tooltip?: string;
44
- actions?: ReactElement;
70
+ icon?: IconProps;
71
+ actions?: ReactElement[];
45
72
  align?: "start" | "center" | "end";
46
73
  locked?: "start" | "end";
47
74
  hidden?: boolean;
75
+ truncate?: boolean;
48
76
  hideable?: boolean;
49
77
  resizable?: boolean;
50
78
  sortable?: boolean;
51
79
  filterable?: boolean;
52
- filterList?: AnyObject[];
80
+ filter?: AnyObject;
81
+ filterOptions?: AnyObject[];
53
82
  width?: string | number;
54
83
  minWidth?: string | number;
55
84
  maxWidth?: string | number;
@@ -76,12 +105,16 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
76
105
  columns: Array<TableColumn<T>>;
77
106
  data: T[];
78
107
  keyProperty?: keyof T;
108
+ groupProperty?: keyof T;
109
+ groupRenderer?: (key: AnyObject) => ReactElement;
79
110
  emptyDisplay?: ReactElement;
111
+
80
112
  /**
81
113
  * checkable rows
82
114
  */
115
+ checked?: AnyObject[];
83
116
  checkableRows?: boolean;
84
- onCheckedChanged?: (rows: Array<keyof T>) => void;
117
+ onCheckedChanged?: (rows: AnyObject[]) => void;
85
118
  /**
86
119
  * sorting
87
120
  */
@@ -98,4 +131,9 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
98
131
 
99
132
  canExpand?: (record: T) => boolean;
100
133
  children?: (record: T) => Elements<AnyObject>;
134
+
135
+ onFilter?: (id: keyof T | string, value?: AnyObject) => void;
136
+
137
+ total?: number;
138
+ onLoadMore?: (lastIndex?: number) => void;
101
139
  }
@@ -47,13 +47,15 @@ export const useResizer = (
47
47
  /** ***************** attach handlers *******************/
48
48
  const box = colEl.getBoundingClientRect();
49
49
  if (colEl.parentElement != null) {
50
- const parentBox = colEl.parentElement?.getBoundingClientRect();
50
+ const parentBox = colEl
51
+ .closest("[data-ref='tableScroller']")
52
+ ?.getBoundingClientRect() as unknown as DOMRect;
51
53
  if (isRtl) {
52
54
  placeholder.style.left = "unset";
53
- placeholder.style.right = `${box.right - parentBox.right}px`;
55
+ placeholder.style.right = `${parentBox.width - box.right}px`;
54
56
  } else {
55
57
  placeholder.style.right = "unset";
56
- placeholder.style.left = `${box.left - parentBox.left}px`;
58
+ placeholder.style.left = `${box.left}px`;
57
59
  }
58
60
  }
59
61
  placeholder.style.width = `${colEl.offsetWidth}px`;
@@ -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" }),