@react-fabric/data 1.0.0-beta.8 → 1.0.1

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 (141) 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 +59 -33
  4. package/dist/cjs/histogram/Histogram.js +41 -26
  5. package/dist/cjs/hooks/useFilteredList.js +100 -17
  6. package/dist/cjs/hooks/usePagination.js +29 -8
  7. package/dist/cjs/hooks/useSelectableList.js +30 -5
  8. package/dist/cjs/i18n/{ar.json.js → ar/data.json.js} +8 -1
  9. package/dist/cjs/i18n/{en.json.js → en/data.json.js} +8 -1
  10. package/dist/cjs/i18n/index.js +4 -4
  11. package/dist/cjs/index.js +30 -0
  12. package/dist/cjs/json/JsonViewer.js +80 -43
  13. package/dist/cjs/pagination/Pagination.js +24 -15
  14. package/dist/cjs/table/AddColumn.js +5 -5
  15. package/dist/cjs/table/BodyCell.js +20 -13
  16. package/dist/cjs/table/BodyRow.js +41 -28
  17. package/dist/cjs/table/CheckboxCell.js +10 -9
  18. package/dist/cjs/table/ColumnFilters.js +100 -0
  19. package/dist/cjs/table/EmptyCell.js +4 -4
  20. package/dist/cjs/table/HeaderCell.js +79 -53
  21. package/dist/cjs/table/HtmlTable.js +84 -0
  22. package/dist/cjs/table/{Context.js → Provider.js} +8 -10
  23. package/dist/cjs/table/Table.js +99 -33
  24. package/dist/cjs/{tree/types.js → table/context.js} +9 -5
  25. package/dist/cjs/table/types.js +29 -0
  26. package/dist/cjs/table/useResizer.js +10 -10
  27. package/dist/cjs/table/useTableColumns.js +4 -4
  28. package/dist/cjs/table/useVirtualTable.js +129 -22
  29. package/dist/cjs/tree/TreeNode.js +66 -46
  30. package/dist/cjs/tree/TreePanel.js +60 -24
  31. package/dist/cjs/tree/useTree.js +212 -47
  32. package/dist/cjs/tree/utils.js +63 -34
  33. package/dist/cjs/utils/getColor.js +58 -0
  34. package/dist/cjs/virtual/Gallery.js +119 -61
  35. package/dist/cjs/virtual/List.js +116 -55
  36. package/dist/cjs/virtual/Scroller.js +16 -8
  37. package/dist/esm/checklist/CheckList.js +61 -35
  38. package/dist/esm/histogram/Histogram.js +43 -28
  39. package/dist/esm/hooks/useFilteredList.js +101 -18
  40. package/dist/esm/hooks/usePagination.js +29 -8
  41. package/dist/esm/hooks/useSelectableList.js +30 -5
  42. package/dist/esm/i18n/{ar.json.js → ar/data.json.js} +8 -2
  43. package/dist/esm/i18n/{en.json.js → en/data.json.js} +8 -2
  44. package/dist/esm/i18n/index.js +2 -2
  45. package/dist/esm/index.js +31 -0
  46. package/dist/esm/json/JsonViewer.js +82 -45
  47. package/dist/esm/pagination/Pagination.js +25 -16
  48. package/dist/esm/table/AddColumn.js +6 -6
  49. package/dist/esm/table/BodyCell.js +21 -14
  50. package/dist/esm/table/BodyRow.js +41 -28
  51. package/dist/esm/table/CheckboxCell.js +11 -10
  52. package/dist/esm/table/ColumnFilters.js +100 -0
  53. package/dist/esm/table/EmptyCell.js +4 -4
  54. package/dist/esm/table/HeaderCell.js +80 -54
  55. package/dist/esm/table/HtmlTable.js +84 -0
  56. package/dist/esm/table/{Context.js → Provider.js} +11 -12
  57. package/dist/esm/table/Table.js +100 -34
  58. package/dist/esm/{tree/types.js → table/context.js} +8 -4
  59. package/dist/esm/table/types.js +29 -0
  60. package/dist/esm/table/useResizer.js +10 -10
  61. package/dist/esm/table/useTableColumns.js +4 -4
  62. package/dist/esm/table/useVirtualTable.js +131 -24
  63. package/dist/esm/tree/TreeNode.js +69 -49
  64. package/dist/esm/tree/TreePanel.js +62 -26
  65. package/dist/esm/tree/useTree.js +215 -50
  66. package/dist/esm/tree/utils.js +64 -35
  67. package/dist/esm/utils/getColor.js +58 -0
  68. package/dist/esm/virtual/Gallery.js +121 -63
  69. package/dist/esm/virtual/List.js +118 -57
  70. package/dist/esm/virtual/Scroller.js +17 -9
  71. package/dist/types/checklist/CheckList.d.ts +25 -6
  72. package/dist/types/histogram/Histogram.d.ts +28 -6
  73. package/dist/types/hooks/useFilteredList.d.ts +62 -5
  74. package/dist/types/hooks/usePagination.d.ts +18 -2
  75. package/dist/types/hooks/useSelectableList.d.ts +24 -1
  76. package/dist/types/index.d.ts +27 -0
  77. package/dist/types/json/JsonViewer.d.ts +23 -3
  78. package/dist/types/pagination/Pagination.d.ts +8 -2
  79. package/dist/types/table/AddColumn.d.ts +2 -2
  80. package/dist/types/table/BodyCell.d.ts +2 -2
  81. package/dist/types/table/BodyRow.d.ts +9 -9
  82. package/dist/types/table/CheckboxCell.d.ts +3 -3
  83. package/dist/types/table/ColumnFilters.d.ts +12 -0
  84. package/dist/types/table/EmptyCell.d.ts +1 -1
  85. package/dist/types/table/HeaderCell.d.ts +1 -1
  86. package/dist/types/table/HtmlTable.d.ts +21 -0
  87. package/dist/types/table/Provider.d.ts +2 -0
  88. package/dist/types/table/Table.d.ts +10 -1
  89. package/dist/types/table/context.d.ts +7 -0
  90. package/dist/types/table/types.d.ts +52 -10
  91. package/dist/types/table/useResizer.d.ts +1 -1
  92. package/dist/types/table/useTableColumns.d.ts +1 -1
  93. package/dist/types/table/useVirtualTable.d.ts +31 -12
  94. package/dist/types/tree/TreeNode.d.ts +1 -1
  95. package/dist/types/tree/TreePanel.d.ts +36 -1
  96. package/dist/types/tree/types.d.ts +75 -28
  97. package/dist/types/tree/useTree.d.ts +8 -12
  98. package/dist/types/tree/utils.d.ts +15 -8
  99. package/dist/types/utils/getColor.d.ts +43 -0
  100. package/dist/types/virtual/Gallery.d.ts +37 -6
  101. package/dist/types/virtual/List.d.ts +38 -6
  102. package/dist/types/virtual/Scroller.d.ts +1 -1
  103. package/package.json +17 -19
  104. package/src/checklist/CheckList.tsx +68 -71
  105. package/src/histogram/Histogram.tsx +47 -48
  106. package/src/hooks/useFilteredList.tsx +111 -28
  107. package/src/hooks/usePagination.tsx +39 -39
  108. package/src/hooks/useSelectableList.ts +45 -33
  109. package/src/i18n/{ar.json → ar/data.json} +5 -0
  110. package/src/i18n/{en.json → en/data.json} +5 -0
  111. package/src/i18n/index.ts +2 -2
  112. package/src/index.ts +23 -17
  113. package/src/json/JsonViewer.tsx +89 -120
  114. package/src/pagination/Pagination.tsx +104 -105
  115. package/src/table/AddColumn.tsx +6 -15
  116. package/src/table/BodyCell.tsx +27 -36
  117. package/src/table/BodyRow.tsx +36 -59
  118. package/src/table/CheckboxCell.tsx +12 -15
  119. package/src/table/ColumnFilters.tsx +93 -0
  120. package/src/table/EmptyCell.tsx +5 -18
  121. package/src/table/HeaderCell.tsx +72 -53
  122. package/src/table/HtmlTable.tsx +106 -0
  123. package/src/table/{Context.tsx → Provider.tsx} +11 -28
  124. package/src/table/Table.tsx +98 -83
  125. package/src/table/context.ts +33 -0
  126. package/src/table/types.ts +53 -27
  127. package/src/table/useResizer.ts +5 -9
  128. package/src/table/useTableColumns.ts +4 -6
  129. package/src/table/useVirtualTable.ts +136 -37
  130. package/src/tree/TreeNode.tsx +91 -86
  131. package/src/tree/TreePanel.tsx +87 -51
  132. package/src/tree/types.ts +85 -46
  133. package/src/tree/useTree.ts +210 -65
  134. package/src/tree/utils.ts +81 -62
  135. package/src/utils/getColor.ts +55 -0
  136. package/src/virtual/Gallery.tsx +129 -120
  137. package/src/virtual/List.tsx +124 -91
  138. package/src/virtual/Scroller.tsx +25 -9
  139. package/dist/cjs/histogram/Histogram.module.css +0 -1
  140. package/dist/esm/histogram/Histogram.module.css +0 -1
  141. package/dist/types/table/Context.d.ts +0 -8
@@ -0,0 +1,33 @@
1
+ /*
2
+ * React Fabric
3
+ * @version: 1.0.0
4
+ *
5
+ *
6
+ * The MIT License (MIT)
7
+ * Copyright (c) 2026 Adarsh Pastakia
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
+ * subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
16
+ * portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import { createContext, use } from "react";
25
+
26
+ interface Context {
27
+ widths: Map<string, number>;
28
+ startResize: (col: HTMLElement) => void;
29
+ }
30
+
31
+ export const TableContext = createContext<Context>({} as Context);
32
+
33
+ export const useTableContext = () => use(TableContext);
@@ -21,49 +21,66 @@
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
- type CssProp,
26
- type Elements,
27
- type IconProps,
28
- type RefProp,
29
- type TestProps,
30
- } from "@react-fabric/core/dist/types/types";
24
+ import { type IconProps } from "@react-fabric/core/dist/types/components/icon/Icon";
25
+ import { type CssProp, type NestedKeys, type RefProp, type TestProps } from "@react-fabric/core/dist/types/types";
31
26
  import { type ReactElement } from "react";
32
27
 
33
28
  export const COL_MIN_WIDTH = "3rem";
34
29
  export const COL_MAX_WIDTH = "64rem";
35
30
  export const COL_DEFAULT_WIDTH = "12rem";
36
31
 
37
- export interface TableColumn<T = KeyValue>
38
- extends CssProp,
39
- IconProps,
40
- TestProps {
41
- id: keyof T | string;
32
+ /**
33
+ * TableColumn interface defines the structure of a column in a table.
34
+ * It includes properties for column identification, display options, alignment,
35
+ * visibility, resizing, sorting, filtering, and rendering.
36
+ *
37
+ * @template T - The type of data the column will represent, defaulting to KeyValue.
38
+ * @property {keyof T | string} id - Unique identifier for the column.
39
+ * @property {"string" | "number" | "boolean" | "datetime" | "date" | "time" | "unknown"} [dataType] - Data type of the column values.
40
+ * @property {string} [label] - Display label for the column header.
41
+ * @property {string} [tooltip] - Tooltip text for the column header.
42
+ * @property {string} [format] - Format string for displaying the column values.
43
+ * @property {"start" | "center" | "end"} [align] - Text alignment within the column.
44
+ * @property {"start" | "end"} [locked] - Indicates if the column is locked at the start or end.
45
+ * @property {boolean} [hidden] - Whether the column is hidden.
46
+ * @property {boolean} [truncate] - Whether to truncate the text in the column.
47
+ * @property {boolean} [hideable] - Whether the column can be hidden by the user.
48
+ * @property {boolean} [resizable] - Whether the column can be resized by the user.
49
+ * @property {boolean} [sortable] - Whether the column can be sorted.
50
+ * @property {boolean} [filterable] - Whether the column can be filtered.
51
+ * @property {AnyObject} [filter] - Filter criteria for the column.
52
+ * @property {ReactElement[]} [actions] - Array of action elements to be displayed in the column.
53
+ * @property {AnyObject[]} [filterOptions] - Options for filtering the column.
54
+ * @property {string | number} [width] - Width of the column.
55
+ * @property {string | number} [minWidth] - Minimum width of the column.
56
+ * @property {string | number} [maxWidth] - Maximum width of the column.
57
+ * @property {KeyValue} [valueMap] - Mapping of values for the column.
58
+ * @property {(value: AnyObject, data: T, index: number) => Elements<AnyObject>} [renderer] - Custom renderer function for the column values.
59
+ */
60
+ export interface TableColumn<T = KeyValue> extends CssProp, TestProps {
61
+ id: NestedKeys<T>;
62
+ field?: string;
42
63
  label?: string;
43
64
  tooltip?: string;
44
- actions?: ReactElement;
65
+ icon?: IconProps;
66
+ actions?: ReactElement[];
45
67
  align?: "start" | "center" | "end";
46
68
  locked?: "start" | "end";
47
69
  hidden?: boolean;
70
+ truncate?: boolean;
48
71
  hideable?: boolean;
49
72
  resizable?: boolean;
50
73
  sortable?: boolean;
51
74
  filterable?: boolean;
52
- filterList?: AnyObject[];
75
+ filter?: string | string[];
76
+ filterOptions?: Array<string | { id: string; label: string }>;
53
77
  width?: string | number;
54
78
  minWidth?: string | number;
55
79
  maxWidth?: string | number;
56
- dataType?:
57
- | "string"
58
- | "number"
59
- | "boolean"
60
- | "datetime"
61
- | "date"
62
- | "time"
63
- | "unknown";
80
+ dataType?: "string" | "number" | "boolean" | "datetime" | "date" | "time" | "unknown";
64
81
  format?: string;
65
- valueMap?: KeyValue;
66
- renderer?: (value: AnyObject, data: T, index: number) => Elements<AnyObject>;
82
+ valueMap?: KeyValue<string>;
83
+ renderer?: (value: AnyObject, data: T, index: number) => React.ReactElement<unknown>;
67
84
  }
68
85
 
69
86
  export interface TableRef {
@@ -75,13 +92,17 @@ export interface TableRef {
75
92
  export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
76
93
  columns: Array<TableColumn<T>>;
77
94
  data: T[];
78
- keyProperty?: keyof T;
95
+ keyProperty?: NestedKeys<T>;
96
+ groupProperty?: NestedKeys<T>;
97
+ groupRenderer?: (key: AnyObject) => ReactElement;
79
98
  emptyDisplay?: ReactElement;
99
+
80
100
  /**
81
101
  * checkable rows
82
102
  */
103
+ checked?: AnyObject[];
83
104
  checkableRows?: boolean;
84
- onCheckedChanged?: (rows: Array<keyof T>) => void;
105
+ onCheckedChanged?: (rows: AnyObject[]) => void;
85
106
  /**
86
107
  * sorting
87
108
  */
@@ -97,5 +118,10 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
97
118
  onScroll?: (firstItem: number) => void;
98
119
 
99
120
  canExpand?: (record: T) => boolean;
100
- children?: (record: T) => Elements<AnyObject>;
121
+ children?: (record: T) => React.ReactElement<unknown>;
122
+
123
+ onFilter?: (id: NestedKeys<T> | string, value?: AnyObject) => void;
124
+
125
+ total?: number;
126
+ onLoadMore?: (lastIndex?: number) => void;
101
127
  }
@@ -21,11 +21,7 @@
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
- export const useResizer = (
25
- colEl: HTMLElement,
26
- ghostEl: HTMLElement,
27
- callback: (width: number) => void,
28
- ) => {
24
+ export function startResizer(colEl: HTMLElement, ghostEl: HTMLElement, callback: (width: number) => void) {
29
25
  const placeholder = ghostEl.firstElementChild as HTMLElement;
30
26
  const isRtl = getComputedStyle(colEl).direction === "rtl";
31
27
 
@@ -47,13 +43,13 @@ export const useResizer = (
47
43
  /** ***************** attach handlers *******************/
48
44
  const box = colEl.getBoundingClientRect();
49
45
  if (colEl.parentElement != null) {
50
- const parentBox = colEl.parentElement?.getBoundingClientRect();
46
+ const parentBox = colEl.closest("[data-ref='tableScroller']")?.getBoundingClientRect() as unknown as DOMRect;
51
47
  if (isRtl) {
52
48
  placeholder.style.left = "unset";
53
- placeholder.style.right = `${box.right - parentBox.right}px`;
49
+ placeholder.style.right = `${parentBox.width - box.right}px`;
54
50
  } else {
55
51
  placeholder.style.right = "unset";
56
- placeholder.style.left = `${box.left - parentBox.left}px`;
52
+ placeholder.style.left = `${box.left}px`;
57
53
  }
58
54
  }
59
55
  placeholder.style.width = `${colEl.offsetWidth}px`;
@@ -63,4 +59,4 @@ export const useResizer = (
63
59
 
64
60
  document.addEventListener("mousemove", onResize);
65
61
  document.addEventListener("mouseup", onResizeEnd);
66
- };
62
+ }
@@ -32,11 +32,9 @@ interface ColumnState {
32
32
  cols: TableColumn[];
33
33
  }
34
34
 
35
- type ColumnActions =
36
- | { type: "columns"; columns: TableColumn[] }
37
- | { type: "hide"; id: string };
35
+ type ColumnActions = { type: "columns"; columns: TableColumn[] } | { type: "hide"; id: string };
38
36
 
39
- export const useTableColumns = (columns: TableColumn[]) => {
37
+ export function useTableColumns(columns: TableColumn[]) {
40
38
  const [state, dispatch] = useReducer(
41
39
  (state: ColumnState, action: ColumnActions) => {
42
40
  if (action.type === "columns") {
@@ -44,7 +42,7 @@ export const useTableColumns = (columns: TableColumn[]) => {
44
42
  }
45
43
  if (action.type === "hide") {
46
44
  const col = state.columns.find((col) => col.id === action.id);
47
- col &&
45
+ if (col)
48
46
  state.columns.splice(state.columns.indexOf(col), 1, {
49
47
  ...col,
50
48
  hidden: col.hidden !== true,
@@ -82,4 +80,4 @@ export const useTableColumns = (columns: TableColumn[]) => {
82
80
  state,
83
81
  onHide: (id: string) => dispatch({ type: "hide", id }),
84
82
  };
85
- };
83
+ }
@@ -21,93 +21,192 @@
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 { defaultRangeExtractor, type Range, useVirtualizer } from "@tanstack/react-virtual";
26
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
26
27
 
27
- interface DataState {
28
+ interface DataGroup {
29
+ __GROUP__: true;
30
+ key: Key;
31
+ open: boolean;
32
+ itemCount: number;
33
+ }
34
+
35
+ interface DataState<T = KeyValue> {
36
+ data: T[];
37
+ items: Array<T | DataGroup>;
38
+ grouped: KeyValue<T[]>;
39
+ groups: DataGroup[];
28
40
  expanded: Key[];
29
41
  checked: Key[];
30
- allChecked: 0 | 1 | 2;
42
+ allChecked: number;
31
43
  }
32
44
 
33
- type DataAction =
45
+ type DataAction<T = KeyValue> =
46
+ | { type: "updateState"; checked: Key[] }
47
+ | { type: "groupExpand"; key: keyof T }
48
+ | { type: "reset"; key?: keyof T; data: T[] }
34
49
  | { type: "expand"; key: Key }
35
50
  | { type: "check"; key: Key }
36
51
  | { type: "checkall" };
37
52
 
38
- export const useVirtualTable = (
39
- data: KeyValue[],
40
- keyProperty?: Key,
41
- onCheckChanged?: (checked: Key[]) => void,
42
- ) => {
53
+ export function useVirtualTable<T extends KeyValue = KeyValue>(
54
+ data: T[],
55
+ options: {
56
+ checked?: Key[];
57
+ keyProperty: keyof T;
58
+ groupProperty?: keyof T;
59
+ loading?: boolean;
60
+ total: number;
61
+ onLoadMore?: (startIndex: number) => void;
62
+ onCheckChanged?: (checked: AnyObject[]) => void;
63
+ },
64
+ ) {
43
65
  // TODO: refactor data into item list, apply grouping if groupColumn provided
44
66
  // TODO: when grouping maintain map of group with children, {___GROUP___:true, label, items, open}
45
-
46
- const scrollerRef = useRef<HTMLDivElement>(null);
47
- const virtualizer = useVirtualizer({
48
- count: data.length,
49
- getScrollElement: () => scrollerRef.current,
50
- estimateSize: () => 24,
51
- });
52
-
53
67
  const [state, dispatch] = useReducer(
54
- (state: DataState, action: DataAction) => {
68
+ (state: DataState<T>, action: DataAction<T>) => {
69
+ if (action.type === "updateState") {
70
+ state.checked = action.checked;
71
+ }
55
72
  if (action.type === "expand") {
56
- if (state.expanded.includes(action.key))
57
- state.expanded.splice(state.expanded.indexOf(action.key), 1);
73
+ if (state.expanded.includes(action.key)) state.expanded.splice(state.expanded.indexOf(action.key), 1);
58
74
  else state.expanded.push(action.key);
59
75
  }
60
76
  if (action.type === "check") {
61
- if (state.checked.includes(action.key))
62
- state.checked.splice(state.checked.indexOf(action.key), 1);
77
+ if (state.checked.includes(action.key)) state.checked.splice(state.checked.indexOf(action.key), 1);
63
78
  else state.checked.push(action.key);
64
- onCheckChanged?.(state.checked);
79
+ options.onCheckChanged?.(state.checked);
65
80
  }
66
81
  if (action.type === "checkall") {
67
82
  if (state.allChecked !== 0) {
68
83
  state.checked = [];
69
- } else if (keyProperty) {
70
- state.checked = data.map((item) => item[keyProperty]);
84
+ } else if (options.keyProperty) {
85
+ state.checked = state.data.map((item) => item[options.keyProperty]);
86
+ }
87
+ options.onCheckChanged?.(state.checked);
88
+ }
89
+ const checkList = state.data.filter((hit) => state.checked.includes(hit[options.keyProperty]));
90
+ if (action.type === "groupExpand") {
91
+ const group = state.groups.find((group) => group.key === action.key);
92
+ if (group) {
93
+ group.open = !group.open;
94
+ state.groups = state.groups.slice();
95
+ state.items = state.groups
96
+ .map((group) => [group, group.open ? state.grouped[group.key] : undefined])
97
+ .flat(2)
98
+ .filter(Boolean) as never;
99
+ }
100
+ }
101
+ if (action.type === "reset") {
102
+ state.data = action.data;
103
+ if (action.key) {
104
+ state.grouped = groupBy<T>(state.data, action.key);
105
+ state.groups = Object.keys(state.grouped)
106
+ .sort(compareValues("asc"))
107
+ .map((group) => ({
108
+ __GROUP__: true,
109
+ open: true,
110
+ key: group,
111
+ itemCount: state.grouped[group].length,
112
+ }));
113
+ state.items = state.groups.map((group) => [group, ...state.grouped[group.key]]).flat();
114
+ } else {
115
+ state.items = state.data;
71
116
  }
72
- onCheckChanged?.(state.checked);
73
117
  }
74
118
  return {
75
119
  ...state,
76
- allChecked:
77
- state.checked.length === data.length
78
- ? 1
79
- : state.checked.length
80
- ? 2
81
- : 0,
82
- } as DataState;
120
+ allChecked: checkList.length === state.data.length ? 1 : state.checked.length ? 2 : 0,
121
+ };
83
122
  },
84
123
  {
124
+ data,
125
+ items: [],
126
+ grouped: {},
127
+ groups: [],
85
128
  checked: [],
86
129
  expanded: [],
87
130
  allChecked: 0,
88
131
  },
89
132
  );
90
133
 
134
+ const activeStickyIndexRef = useRef(0);
135
+ const stickyIndexes = useMemo<number[]>(
136
+ () => state.items.map((hit, idx) => ("__GROUP__" in hit ? idx : undefined)).filter(Boolean) as number[],
137
+ [state.items],
138
+ );
139
+ const isActiveSticky = (index: number) => activeStickyIndexRef.current === index;
140
+
141
+ const [hasActiveSticky, setHasActiveSticky] = useState(false);
142
+ const scrollerRef = useRef<HTMLDivElement>(null);
143
+ const virtualizer = useVirtualizer({
144
+ count: state.items.length,
145
+ getScrollElement: () => scrollerRef.current,
146
+ estimateSize: () => 32,
147
+ rangeExtractor: useCallback(
148
+ (range: Range) => {
149
+ activeStickyIndexRef.current = [...stickyIndexes].reverse().find((index) => range.startIndex > index) ?? 0;
150
+
151
+ setHasActiveSticky(range.startIndex !== activeStickyIndexRef.current);
152
+
153
+ const next = new Set([activeStickyIndexRef.current, ...defaultRangeExtractor(range)]);
154
+
155
+ return [...next];
156
+ },
157
+ [stickyIndexes],
158
+ ),
159
+ });
160
+
91
161
  const virtualItems = virtualizer.getVirtualItems();
92
162
 
93
163
  const getData = useCallback(
94
164
  (index: number) => {
95
- return data[index];
165
+ return state.items[index];
96
166
  },
97
- [data],
167
+ [state.items],
98
168
  );
99
169
 
170
+ useEffect(() => {
171
+ dispatch({ type: "reset", key: options.groupProperty, data });
172
+ }, [data, options.groupProperty]);
173
+
174
+ useEffect(() => {
175
+ dispatch({ type: "updateState", checked: options.checked ?? [] });
176
+ }, [options.checked]);
177
+
178
+ useEffect(() => {
179
+ if (options.onLoadMore) {
180
+ const firstItem = virtualItems[0]?.index;
181
+ const lastItem = virtualItems?.slice(-1).pop()?.index;
182
+ const canLoadPrevPage = firstItem !== undefined && state.items[firstItem] === null;
183
+ const canLoadNextPage =
184
+ lastItem !== undefined && lastItem === state.items.length - 1 && state.items.length < options.total;
185
+ const db = debounce(options.onLoadMore);
186
+ if (canLoadPrevPage) {
187
+ db(firstItem);
188
+ } else if (!options.loading && canLoadNextPage) {
189
+ db(lastItem + 1);
190
+ }
191
+ return () => db.cancel();
192
+ }
193
+ // eslint-disable-next-line @eslint-react/exhaustive-deps
194
+ }, [options.onLoadMore, virtualItems]);
195
+
100
196
  return {
101
197
  scrollerRef,
102
198
  items: virtualItems,
103
199
  checkState: state,
104
200
  getData,
105
201
  virtualizer,
202
+ hasActiveSticky,
203
+ isActiveSticky,
106
204
  top: virtualizer.range?.startIndex,
107
205
  totalSize: virtualizer.getTotalSize,
108
206
  measureElement: virtualizer.measureElement,
207
+ toggleGroupExpand: (key: Key) => dispatch({ type: "groupExpand", key }),
109
208
  toggleExpand: (key: Key) => dispatch({ type: "expand", key }),
110
209
  toggleChecked: (key: Key) => dispatch({ type: "check", key }),
111
210
  toggleAllChecked: () => dispatch({ type: "checkall" }),
112
211
  };
113
- };
212
+ }