@etsoo/react 1.8.51 → 1.8.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.
@@ -1,12 +1,6 @@
1
1
  import { DataTypes } from "@etsoo/shared";
2
2
  import React from "react";
3
- import {
4
- Align,
5
- GridChildComponentProps,
6
- GridOnItemsRenderedProps,
7
- VariableSizeGrid,
8
- VariableSizeGridProps
9
- } from "react-window";
3
+ import { Grid, GridProps, useGridRef } from "react-window";
10
4
  import {
11
5
  GridJsonData,
12
6
  GridLoadDataProps,
@@ -14,32 +8,20 @@ import {
14
8
  GridLoaderPartialStates,
15
9
  GridLoaderStates
16
10
  } from "./GridLoader";
17
- import { GridMethodRef } from "./GridMethodRef";
11
+ import { GridMethodRef, ScrollToRowParam } from "./GridMethodRef";
12
+ import { useCombinedRefs } from "../uses/useCombinedRefs";
18
13
 
19
- export type ScrollerGridItemRendererProps<T> = Omit<
20
- GridChildComponentProps<T>,
21
- "data"
22
- > & {
23
- /**
24
- * Selected items
25
- */
26
- selectedItems: T[];
27
-
28
- /**
29
- * Set items for rerenderer
30
- * @param callback Callback
31
- */
32
- setItems: (
33
- callback: (
34
- items: T[],
35
- ref: ScrollerGridForwardRef<T>
36
- ) => T[] | undefined | void
37
- ) => void;
14
+ type ScrollerGridCellrops<T extends object> = {
15
+ rows: T[];
16
+ states: GridLoaderStates<T>;
17
+ };
38
18
 
39
- /**
40
- * Data
41
- */
42
- data?: T;
19
+ export type ScrollToCellParam = {
20
+ behavior?: ScrollToRowParam["behavior"];
21
+ columnAlign?: ScrollToRowParam["align"];
22
+ columnIndex: number;
23
+ rowAlign?: ScrollToRowParam["align"];
24
+ rowIndex: number;
43
25
  };
44
26
 
45
27
  /**
@@ -49,7 +31,10 @@ export type ScrollerGridProps<
49
31
  T extends object,
50
32
  P extends GridJsonData = GridLoadDataProps
51
33
  > = GridLoader<T, P> &
52
- Omit<VariableSizeGridProps<T>, "children" | "rowCount" | "rowHeight"> & {
34
+ Omit<
35
+ GridProps<ScrollerGridCellrops<T>>,
36
+ "cellProps" | "overscanCount" | "rowCount"
37
+ > & {
53
38
  /**
54
39
  * Footer renderer
55
40
  */
@@ -64,16 +49,14 @@ export type ScrollerGridProps<
64
49
  headerRenderer?: (states: GridLoaderStates<T>) => React.ReactNode;
65
50
 
66
51
  /**
67
- * Id field
52
+ * Height of the grid
68
53
  */
69
- idField?: DataTypes.Keys<T>;
54
+ height?: number | string;
70
55
 
71
56
  /**
72
- * Item renderer
57
+ * Id field
73
58
  */
74
- itemRenderer: (
75
- props: ScrollerGridItemRendererProps<T>
76
- ) => React.ReactElement;
59
+ idField?: DataTypes.Keys<T>;
77
60
 
78
61
  /**
79
62
  * Methods
@@ -86,9 +69,9 @@ export type ScrollerGridProps<
86
69
  onSelectChange?: (selectedItems: T[]) => void;
87
70
 
88
71
  /**
89
- * Returns the height of the specified row.
72
+ * Width of the grid
90
73
  */
91
- rowHeight?: ((index: number) => number) | number;
74
+ width?: number | string;
92
75
  };
93
76
 
94
77
  /**
@@ -96,24 +79,16 @@ export type ScrollerGridProps<
96
79
  */
97
80
  export interface ScrollerGridForwardRef<T> extends GridMethodRef<T> {
98
81
  /**
99
- * Scroll to the specified offsets
82
+ * Scroll to the cell
83
+ * @param param Parameters to control
100
84
  */
101
- scrollTo(params: { scrollLeft: number; scrollTop: number }): void;
102
-
103
- scrollToItem(params: {
104
- align?: Align | undefined;
105
- columnIndex?: number | undefined;
106
- rowIndex?: number | undefined;
107
- }): void;
85
+ scrollToCell(param: ScrollToCellParam): void;
108
86
 
109
87
  /**
110
- * Scroll to the specified item
88
+ * Scroll to the cell
89
+ * @param param Parameters to control
111
90
  */
112
- scrollToItem(params: {
113
- align?: Align | undefined;
114
- columnIndex?: number | undefined;
115
- rowIndex?: number | undefined;
116
- }): void;
91
+ scrollToColumn(param: ScrollToRowParam): void;
117
92
 
118
93
  /**
119
94
  * Select the item
@@ -133,21 +108,6 @@ export interface ScrollerGridForwardRef<T> extends GridMethodRef<T> {
133
108
  * @param checked Checked
134
109
  */
135
110
  selectItem(item: any, checked: boolean): void;
136
-
137
- /**
138
- *
139
- * @param index
140
- * @param shouldForceUpdate
141
- */
142
- resetAfterColumnIndex(index: number, shouldForceUpdate?: boolean): void;
143
-
144
- resetAfterIndices(params: {
145
- columnIndex: number;
146
- rowIndex: number;
147
- shouldForceUpdate?: boolean | undefined;
148
- }): void;
149
-
150
- resetAfterRowIndex(index: number, shouldForceUpdate?: boolean): void;
151
111
  }
152
112
 
153
113
  /**
@@ -162,32 +122,45 @@ export const ScrollerGrid = <T extends object>(props: ScrollerGridProps<T>) => {
162
122
  defaultOrderBy,
163
123
  footerRenderer,
164
124
  headerRenderer,
165
- itemRenderer,
125
+ height = "100%",
126
+ gridRef,
127
+ width = "100%",
128
+ style = {},
166
129
  idField = "id" as DataTypes.Keys<T>,
167
130
  loadBatchSize,
168
131
  loadData,
169
132
  mRef,
170
- onItemsRendered,
133
+ onCellsRendered,
171
134
  onSelectChange,
172
135
  rowHeight = 53,
173
- threshold = 6,
174
- width,
136
+ threshold = 3,
175
137
  onInitLoad,
176
138
  onUpdateRows,
177
139
  ...rest
178
140
  } = props;
179
141
 
142
+ // Style
143
+ Object.assign(style, {
144
+ width,
145
+ height,
146
+ overflowX: "hidden"
147
+ });
148
+
149
+ // Refs
150
+ const localRef = useGridRef(null);
151
+ const refs = useCombinedRefs(gridRef, localRef);
152
+
180
153
  // Rows
181
154
  const [rows, updateRows] = React.useState<T[]>([]);
182
155
  const setRows = (rows: T[], reset: boolean = false) => {
183
- refs.current.loadedItems = rows.length;
156
+ stateRefs.current.loadedItems = rows.length;
184
157
  updateRows(rows);
185
158
 
186
- if (!reset && onUpdateRows) onUpdateRows(rows, refs.current);
159
+ if (!reset && onUpdateRows) onUpdateRows(rows, stateRefs.current);
187
160
  };
188
161
 
189
- // Refs
190
- const refs = React.useRef<GridLoaderStates<T>>({
162
+ // State Refs
163
+ const stateRefs = React.useRef<GridLoaderStates<T>>({
191
164
  queryPaging: {
192
165
  currentPage: 0,
193
166
  orderBy: defaultOrderBy,
@@ -201,72 +174,71 @@ export const ScrollerGrid = <T extends object>(props: ScrollerGridProps<T>) => {
201
174
  idCache: {}
202
175
  });
203
176
 
204
- const ref = React.useRef<VariableSizeGrid<T>>(null);
205
-
206
177
  // Load data
207
178
  const loadDataLocal = (pageAdd: number = 1) => {
208
179
  // Prevent multiple loadings
209
180
  if (
210
- !refs.current.hasNextPage ||
211
- refs.current.isNextPageLoading ||
212
- refs.current.isMounted === false
181
+ !stateRefs.current.hasNextPage ||
182
+ stateRefs.current.isNextPageLoading ||
183
+ stateRefs.current.isMounted === false
213
184
  )
214
185
  return;
215
186
 
216
187
  // Update state
217
- refs.current.isNextPageLoading = true;
188
+ stateRefs.current.isNextPageLoading = true;
218
189
 
219
190
  // Parameters
220
- const { queryPaging, data } = refs.current;
191
+ const { queryPaging, data } = stateRefs.current;
221
192
 
222
193
  const loadProps: GridLoadDataProps = {
223
194
  queryPaging,
224
195
  data
225
196
  };
226
197
 
227
- loadData(loadProps, refs.current.lastItem).then((result) => {
228
- if (result == null || refs.current.isMounted === false) {
198
+ loadData(loadProps, stateRefs.current.lastItem).then((result) => {
199
+ if (result == null || stateRefs.current.isMounted === false) {
229
200
  return;
230
201
  }
231
- refs.current.isMounted = true;
202
+ stateRefs.current.isMounted = true;
232
203
 
233
204
  const newItems = result.length;
234
- refs.current.lastLoadedItems = newItems;
235
- refs.current.lastItem = result.at(-1);
236
- refs.current.isNextPageLoading = false;
237
- refs.current.hasNextPage = newItems >= refs.current.queryPaging.batchSize;
205
+ stateRefs.current.lastLoadedItems = newItems;
206
+ stateRefs.current.lastItem = result.at(-1);
207
+ stateRefs.current.isNextPageLoading = false;
208
+ stateRefs.current.hasNextPage =
209
+ newItems >= stateRefs.current.queryPaging.batchSize;
238
210
 
239
211
  if (pageAdd === 0) {
240
212
  // New items
241
- const newRows = refs.current.lastLoadedItems
213
+ const newRows = stateRefs.current.lastLoadedItems
242
214
  ? [...rows]
243
215
  .splice(
244
- rows.length - refs.current.lastLoadedItems,
245
- refs.current.lastLoadedItems
216
+ rows.length - stateRefs.current.lastLoadedItems,
217
+ stateRefs.current.lastLoadedItems
246
218
  )
247
219
  .concat(result)
248
220
  : result;
249
221
 
250
- refs.current.idCache = {};
222
+ stateRefs.current.idCache = {};
251
223
  for (const row of newRows) {
252
224
  const id = row[idField] as any;
253
- refs.current.idCache[id] = null;
225
+ stateRefs.current.idCache[id] = null;
254
226
  }
255
227
 
256
228
  // Update rows
257
229
  setRows(newRows);
258
230
  } else {
259
231
  // Set current page
260
- if (refs.current.queryPaging.currentPage == null)
261
- refs.current.queryPaging.currentPage = pageAdd;
262
- else refs.current.queryPaging.currentPage += pageAdd;
232
+ if (stateRefs.current.queryPaging.currentPage == null)
233
+ stateRefs.current.queryPaging.currentPage = pageAdd;
234
+ else stateRefs.current.queryPaging.currentPage += pageAdd;
263
235
 
264
236
  // Update rows, avoid duplicate items
265
237
  const newRows = [...rows];
266
238
 
267
239
  for (const item of result) {
268
240
  const id = item[idField] as any;
269
- if (refs.current.idCache[id] === undefined) {
241
+ if (stateRefs.current.idCache[id] === undefined) {
270
242
  newRows.push(item);
271
243
  }
272
244
  }
@@ -276,44 +248,6 @@ export const ScrollerGrid = <T extends object>(props: ScrollerGridProps<T>) => {
276
248
  });
277
249
  };
278
250
 
279
- // Item renderer
280
- const itemRendererLocal = (
281
- itemProps: GridChildComponentProps<T>,
282
- state: GridLoaderStates<T>
283
- ) => {
284
- // Custom render
285
- const data =
286
- itemProps.rowIndex < rows.length ? rows[itemProps.rowIndex] : undefined;
287
- return itemRenderer({
288
- ...itemProps,
289
- data,
290
- selectedItems: state.selectedItems,
291
- setItems: (
292
- callback: (
293
- items: T[],
294
- ref: ScrollerGridForwardRef<T>
295
- ) => T[] | undefined | void
296
- ) => {
297
- const result = callback(rows, instance);
298
- if (result == null) return;
299
- setRows(result);
300
- }
301
- });
302
- };
303
-
304
- // Local items renderer callback
305
- const onItemsRenderedLocal = (props: GridOnItemsRenderedProps) => {
306
- // No items, means no necessary to load more data during reset
307
- const itemCount = rows.length;
308
- if (itemCount > 0 && props.visibleRowStopIndex + threshold > itemCount) {
309
- // Auto load next page
310
- loadDataLocal();
311
- }
312
-
313
- // Custom
314
- if (onItemsRendered) onItemsRendered(props);
315
- };
316
-
317
251
  // Reset the state and load again
318
252
  const reset = (add?: GridLoaderPartialStates<T>, items: T[] = []) => {
319
253
  const { queryPaging, ...rest } = add ?? {};
@@ -326,124 +260,99 @@ export const ScrollerGrid = <T extends object>(props: ScrollerGridProps<T>) => {
326
260
  lastItem: undefined,
327
261
  ...rest
328
262
  };
329
- Object.assign(refs.current, resetState);
330
- Object.assign(refs.current.queryPaging, {
263
+ Object.assign(stateRefs.current, resetState);
264
+ Object.assign(stateRefs.current.queryPaging, {
331
265
  currentPage: 0,
332
266
  ...queryPaging
333
267
  });
334
268
 
335
269
  // Reset items
336
- if (refs.current.isMounted !== false) setRows(items, true);
270
+ if (stateRefs.current.isMounted !== false) setRows(items, true);
337
271
  };
338
272
 
339
- const instance: ScrollerGridForwardRef<T> = {
340
- delete(index) {
341
- const item = rows.at(index);
342
- if (item) {
273
+ React.useImperativeHandle(
274
+ mRef,
275
+ () => ({
276
+ delete(index) {
277
+ const item = rows.at(index);
278
+ if (item) {
279
+ const newRows = [...rows];
280
+ newRows.splice(index, 1);
281
+ setRows(newRows);
282
+ }
283
+ return item;
284
+ },
285
+ insert(item, start) {
343
286
  const newRows = [...rows];
344
- newRows.splice(index, 1);
287
+ newRows.splice(start, 0, item);
345
288
  setRows(newRows);
346
- }
347
- return item;
348
- },
349
- insert(item, start) {
350
- const newRows = [...rows];
351
- newRows.splice(start, 0, item);
352
- setRows(newRows);
353
- },
354
- scrollTo(params: { scrollLeft: number; scrollTop: number }) {
355
- ref.current?.scrollTo(params);
356
- },
357
- scrollToItem(params: {
358
- align?: Align | undefined;
359
- columnIndex?: number | undefined;
360
- rowIndex?: number | undefined;
361
- }) {
362
- ref.current?.scrollToItem(params);
363
- },
364
- scrollToRef(scrollOffset: number): void {
365
- ref.current?.scrollTo({ scrollLeft: 0, scrollTop: scrollOffset });
366
- },
367
-
368
- scrollToItemRef(index: number, align?: Align): void {
369
- ref.current?.scrollToItem({ rowIndex: index, align });
370
- },
371
- select(rowIndex: number) {
372
- // Select only one item
373
- const selectedItems = refs.current.selectedItems;
374
- selectedItems[0] = rows[rowIndex];
375
-
376
- if (onSelectChange) onSelectChange(selectedItems);
377
- },
378
- selectAll(checked: boolean) {
379
- const selectedItems = refs.current.selectedItems;
289
+ },
290
+ refresh(): void {
291
+ loadDataLocal(0);
292
+ },
293
+ reset,
294
+ scrollToCell(param: ScrollToCellParam): void {
295
+ localRef.current?.scrollToCell(param);
296
+ },
297
+ scrollToColumn(param: ScrollToRowParam): void {
298
+ localRef.current?.scrollToColumn(param);
299
+ },
300
+ scrollToRow(param: ScrollToRowParam): void {
301
+ localRef.current?.scrollToRow(param);
302
+ },
303
+ select(rowIndex: number) {
304
+ // Select only one item
305
+ const selectedItems = stateRefs.current.selectedItems;
306
+ selectedItems[0] = rows[rowIndex];
307
+
308
+ if (onSelectChange) onSelectChange(selectedItems);
309
+ },
310
+ selectAll(checked: boolean) {
311
+ const selectedItems = stateRefs.current.selectedItems;
312
+
313
+ rows.forEach((row) => {
314
+ const index = selectedItems.findIndex(
315
+ (selectedItem) => selectedItem[idField] === row[idField]
316
+ );
317
+
318
+ if (checked) {
319
+ if (index === -1) selectedItems.push(row);
320
+ } else if (index !== -1) {
321
+ selectedItems.splice(index, 1);
322
+ }
323
+ });
380
324
 
381
- rows.forEach((row) => {
325
+ if (onSelectChange) onSelectChange(selectedItems);
326
+ },
327
+ selectItem(item: T, checked: boolean) {
328
+ const selectedItems = stateRefs.current.selectedItems;
382
329
  const index = selectedItems.findIndex(
383
- (selectedItem) => selectedItem[idField] === row[idField]
330
+ (selectedItem) => selectedItem[idField] === item[idField]
384
331
  );
385
332
 
386
333
  if (checked) {
387
- if (index === -1) selectedItems.push(row);
388
- } else if (index !== -1) {
389
- selectedItems.splice(index, 1);
334
+ if (index === -1) selectedItems.push(item);
335
+ } else {
336
+ if (index !== -1) selectedItems.splice(index, 1);
390
337
  }
391
- });
392
338
 
393
- if (onSelectChange) onSelectChange(selectedItems);
394
- },
395
- selectItem(item: T, checked: boolean) {
396
- const selectedItems = refs.current.selectedItems;
397
- const index = selectedItems.findIndex(
398
- (selectedItem) => selectedItem[idField] === item[idField]
399
- );
400
-
401
- if (checked) {
402
- if (index === -1) selectedItems.push(item);
403
- } else {
404
- if (index !== -1) selectedItems.splice(index, 1);
339
+ if (onSelectChange) onSelectChange(selectedItems);
405
340
  }
406
-
407
- if (onSelectChange) onSelectChange(selectedItems);
408
- },
409
- reset,
410
- resetAfterColumnIndex(index: number, shouldForceUpdate?: boolean) {
411
- ref.current?.resetAfterColumnIndex(index, shouldForceUpdate);
412
- },
413
- resetAfterIndices(params: {
414
- columnIndex: number;
415
- rowIndex: number;
416
- shouldForceUpdate?: boolean | undefined;
417
- }) {
418
- ref.current?.resetAfterIndices(params);
419
- },
420
- resetAfterRowIndex(index: number, shouldForceUpdate?: boolean) {
421
- ref.current?.resetAfterRowIndex(index, shouldForceUpdate);
422
- }
423
- };
424
-
425
- React.useImperativeHandle(mRef, () => instance, [rows]);
426
-
427
- // Force update to work with the new width and rowHeight
428
- React.useEffect(() => {
429
- ref.current?.resetAfterIndices({
430
- columnIndex: 0,
431
- rowIndex: 0,
432
- shouldForceUpdate: true
433
- });
434
- }, [width, rowHeight]);
341
+ }),
342
+ [rows]
343
+ );
435
344
 
436
345
  // Rows
437
- const rowLength = rows.length;
438
-
439
- // Row count
440
- const rowCount = refs.current.hasNextPage ? rowLength + 1 : rowLength;
346
+ const rowCount = rows.length;
441
347
 
442
348
  React.useEffect(() => {
443
349
  // Auto load data when current page is 0
444
- if (refs.current.queryPaging.currentPage === 0 && refs.current.autoLoad) {
350
+ if (
351
+ stateRefs.current.queryPaging.currentPage === 0 &&
352
+ stateRefs.current.autoLoad
353
+ ) {
445
354
  const initItems =
446
- onInitLoad == null ? undefined : onInitLoad(ref.current);
355
+ onInitLoad == null ? undefined : onInitLoad(stateRefs.current);
447
356
  if (initItems) reset(initItems[1], initItems[0]);
448
357
  else loadDataLocal();
449
358
  }
@@ -451,33 +360,36 @@ export const ScrollerGrid = <T extends object>(props: ScrollerGridProps<T>) => {
451
360
 
452
361
  React.useEffect(() => {
453
362
  return () => {
454
- refs.current.isMounted = false;
363
+ stateRefs.current.isMounted = false;
455
364
  };
456
365
  }, []);
457
366
 
458
367
  // Layout
459
368
  return (
460
369
  <React.Fragment>
461
- {headerRenderer && headerRenderer(refs.current)}
462
- <VariableSizeGrid<T>
463
- itemKey={({ columnIndex, rowIndex, data }) => {
464
- if (data == null) return [rowIndex, columnIndex].join(",");
465
- // ${data[idField]}-${rowIndex} always unique but no cache for the same item
466
- return [`${data[idField]}`, columnIndex].join(",");
370
+ {headerRenderer && headerRenderer(stateRefs.current)}
371
+ <Grid<ScrollerGridCellrops<T>>
372
+ cellProps={{ rows, states: stateRefs.current }}
373
+ gridRef={refs}
374
+ onCellsRendered={(visibleCells, allCells) => {
375
+ // No items, means no necessary to load more data during reset
376
+ if (
377
+ rowCount > 0 &&
378
+ visibleCells.rowStopIndex + threshold > rowCount
379
+ ) {
380
+ // Auto load next page
381
+ loadDataLocal();
382
+ }
383
+
384
+ onCellsRendered?.(visibleCells, allCells);
467
385
  }}
468
- onItemsRendered={onItemsRenderedLocal}
469
- ref={ref}
386
+ overscanCount={threshold}
387
+ rowHeight={rowHeight}
470
388
  rowCount={rowCount}
471
- rowHeight={
472
- typeof rowHeight === "function" ? rowHeight : () => rowHeight
473
- }
474
- style={{ overflowX: "hidden" }}
475
- width={width}
389
+ style={style}
476
390
  {...rest}
477
- >
478
- {(props) => itemRendererLocal(props, refs.current)}
479
- </VariableSizeGrid>
480
- {footerRenderer && footerRenderer(rows, refs.current)}
391
+ />
392
+ {footerRenderer && footerRenderer(rows, stateRefs.current)}
481
393
  </React.Fragment>
482
394
  );
483
395
  };