beautiful-grid 1.0.1 → 1.0.3

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.
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react';
3
3
  import { useShallow } from 'zustand/react/shallow';
4
4
  import { useAppStore } from '../store';
5
5
  import { BGridDataItemStatus, } from '../types';
6
- import { getCellValueByRowKey, getFrozenColumnsWidth, getVisibleScrollableRowRange, isCheckboxValueChecked, markCellEdited, markCellValueChanged, resolveLogicalCell, shouldRenderBottomBar, } from '../utils';
6
+ import { getCellValueByRowKey, getFrozenColumnsWidth, getVisibleScrollableRowRange, getVirtualScrollWindowMetrics, getVirtualScrollWindowPosition, isCheckboxValueChecked, markCellEdited, markCellValueChanged, rebaseVirtualScrollWindow, resolveLogicalCell, shouldRenderBottomBar, } from '../utils';
7
7
  import { clamp, ensureCellVisible } from '../utils/coordinate';
8
8
  import Loading from './Loading';
9
9
  import TableBody from './TableBody';
@@ -183,6 +183,23 @@ function Table(props) {
183
183
  const trHeight = itemHeight + itemPadding * 2;
184
184
  const scrollableBodyHeight = Math.max(contentBodyHeight - frozenRowsHeight, 0);
185
185
  const mainViewportWidth = Math.max(width - (frozenColumnsWidth ?? 0), 0);
186
+ const logicalBodyContentHeight = data.length * trHeight;
187
+ const virtualScrollWindowMetrics = React.useMemo(() => getVirtualScrollWindowMetrics({
188
+ logicalContentHeight: logicalBodyContentHeight,
189
+ viewportHeight: contentBodyHeight,
190
+ enabled: scrollbar.variant !== 'native',
191
+ }), [contentBodyHeight, logicalBodyContentHeight, scrollbar.variant]);
192
+ const [virtualScrollBase, setVirtualScrollBase] = React.useState(0);
193
+ const virtualScrollBaseRef = useRef(0);
194
+ const containerRef = useRef(null);
195
+ const searchPopoverRef = useRef(null);
196
+ const editorPortalRef = useRef(null);
197
+ const bodyContainerRef = useRef(null);
198
+ const scrollContainerRef = useRef(null);
199
+ const logicalVerticalScrollState = React.useMemo(() => ({
200
+ scrollTop,
201
+ scrollHeight: virtualScrollWindowMetrics.logicalContentHeight,
202
+ }), [scrollTop, virtualScrollWindowMetrics.logicalContentHeight]);
186
203
  const hasMultiCellSelection = React.useMemo(() => cellSelectionRanges.length > 1 ||
187
204
  cellSelectionRanges.some(range => {
188
205
  const normalized = normalizeCellSelectionRange(range);
@@ -273,7 +290,7 @@ function Table(props) {
273
290
  top: topQuadrant ? 0 : scrollTop,
274
291
  width: leftQuadrant ? frozenColumnsWidth ?? 0 : mainViewportWidth,
275
292
  height: topQuadrant ? frozenRowsHeight : scrollableBodyHeight,
276
- } }));
293
+ }, offsetTop: !topQuadrant && virtualScrollWindowMetrics.enabled ? virtualScrollBase : 0 }));
277
294
  };
278
295
  const visibleScrollableRows = React.useMemo(() => getVisibleScrollableRowRange({
279
296
  scrollTop,
@@ -288,15 +305,14 @@ function Table(props) {
288
305
  startRowIndex: visibleScrollableRows.startRowIndex,
289
306
  endRowIndex: visibleScrollableRows.endRowIndex,
290
307
  }), [visibleScrollableRows.endRowIndex, visibleScrollableRows.startRowIndex]);
291
- const frozenScrollableBodyStyle = React.useMemo(() => ({ top: visibleScrollableRows.paddingTop }), [visibleScrollableRows.paddingTop]);
292
- const containerRef = useRef(null);
293
- const searchPopoverRef = useRef(null);
308
+ const frozenScrollableBodyStyle = React.useMemo(() => ({
309
+ top: virtualScrollWindowMetrics.enabled
310
+ ? visibleScrollableRows.paddingTop - virtualScrollBase
311
+ : visibleScrollableRows.paddingTop,
312
+ }), [virtualScrollBase, virtualScrollWindowMetrics.enabled, visibleScrollableRows.paddingTop]);
294
313
  const warnedContextMenuIdsRef = useRef(new Set());
295
314
  const warnedContextMenuFactoryRef = useRef(false);
296
- const editorPortalRef = useRef(null);
297
315
  const editorPortalContext = React.useMemo(() => ({ gridRef: containerRef, portalRef: editorPortalRef }), []);
298
- const bodyContainerRef = useRef(null);
299
- const scrollContainerRef = useRef(null);
300
316
  const rowReorderController = useRowReorderController({
301
317
  containerRef,
302
318
  bodyContainerRef,
@@ -404,7 +420,8 @@ function Table(props) {
404
420
  const stickyBottomHeight = summary?.position === 'bottom' ? summaryHeight : 0;
405
421
  const stickyFixedHeight = stickyTopHeight + stickyBottomHeight;
406
422
  const scrollViewportHeight = contentBodyHeight + stickyFixedHeight;
407
- const scrollPlaneHeight = stickyFixedHeight + frozenRowsHeight + visibleScrollableRows.scrollContentHeight;
423
+ const physicalScrollableRowsHeight = Math.max(virtualScrollWindowMetrics.physicalContentHeight - frozenRowsHeight, 0);
424
+ const scrollPlaneHeight = stickyFixedHeight + virtualScrollWindowMetrics.physicalContentHeight;
408
425
  const scrollPlaneContentWidth = (frozenColumnsWidth ?? 0) + scrollableColumnsWidth;
409
426
  const scrollPlaneWidth = Math.max(width, scrollPlaneContentWidth);
410
427
  const customVerticalScrollbarGutter = scrollbar.vertical.visible && scrollbar.variant === 'classic'
@@ -415,7 +432,18 @@ function Table(props) {
415
432
  const scrollPlaneMinWidth = customVerticalScrollbarGutter
416
433
  ? `max(100%, calc(${scrollPlaneContentWidth}px + ${customVerticalScrollbarGutter}))`
417
434
  : scrollPlaneWidth;
418
- const scrollbarMetrics = useScrollbarMetrics(scrollContainerRef, [data.length, itemHeight, itemPadding, contentBodyHeight, width], scrollPlaneContentWidth, stickyFixedHeight);
435
+ const measuredScrollbarMetrics = useScrollbarMetrics(scrollContainerRef, [data.length, itemHeight, itemPadding, contentBodyHeight, width], scrollPlaneContentWidth, stickyFixedHeight);
436
+ const scrollbarMetrics = React.useMemo(() => virtualScrollWindowMetrics.enabled
437
+ ? {
438
+ ...measuredScrollbarMetrics,
439
+ vertical: {
440
+ viewportSize: contentBodyHeight,
441
+ contentSize: virtualScrollWindowMetrics.logicalContentHeight,
442
+ maxScroll: virtualScrollWindowMetrics.logicalMaxScroll,
443
+ hasOverflow: virtualScrollWindowMetrics.logicalMaxScroll > 1,
444
+ },
445
+ }
446
+ : measuredScrollbarMetrics, [contentBodyHeight, measuredScrollbarMetrics, virtualScrollWindowMetrics]);
419
447
  const measuredVerticalScrollbarGutter = customVerticalScrollbarGutter && scrollbarMetrics.horizontal.hasOverflow
420
448
  ? Math.max(scrollbarMetrics.horizontal.contentSize - scrollPlaneContentWidth, 0)
421
449
  : 0;
@@ -435,11 +463,6 @@ function Table(props) {
435
463
  scrollContainerRef.current.scrollLeft = offset;
436
464
  }
437
465
  }, []);
438
- const handleScrollTopChange = useCallback((offset) => {
439
- if (scrollContainerRef.current) {
440
- scrollContainerRef.current.scrollTop = offset;
441
- }
442
- }, []);
443
466
  const latestScrollRef = useRef({ top: scrollTop, left: scrollLeft });
444
467
  const latestCommittedScrollRef = useRef({ top: scrollTop, left: scrollLeft });
445
468
  const scrollRafRef = useRef(null);
@@ -456,6 +479,7 @@ function Table(props) {
456
479
  frozenRowCount,
457
480
  frozenColumnsWidth: frozenColumnsWidth ?? 0,
458
481
  frozenRowsHeight,
482
+ scrollTop,
459
483
  trHeight,
460
484
  });
461
485
  useEffect(() => {
@@ -466,9 +490,10 @@ function Table(props) {
466
490
  frozenRowCount,
467
491
  frozenColumnsWidth: frozenColumnsWidth ?? 0,
468
492
  frozenRowsHeight,
493
+ scrollTop,
469
494
  trHeight,
470
495
  };
471
- }, [columns, data.length, props.frozenColumnIndex, frozenColumnsWidth, frozenRowCount, frozenRowsHeight, trHeight]);
496
+ }, [columns, data.length, props.frozenColumnIndex, frozenColumnsWidth, frozenRowCount, frozenRowsHeight, scrollTop, trHeight]);
472
497
  const markScrollActive = useCallback(() => {
473
498
  containerRef.current?.setAttribute('data-bgrid-scrolling', 'true');
474
499
  if (scrollIdleTimerRef.current !== null)
@@ -478,6 +503,27 @@ function Table(props) {
478
503
  containerRef.current?.removeAttribute('data-bgrid-scrolling');
479
504
  }, SCROLL_IDLE_DELAY);
480
505
  }, []);
506
+ const commitScroll = useCallback((top, left) => {
507
+ const scrollContainer = scrollContainerRef.current;
508
+ if (!scrollContainer)
509
+ return;
510
+ const position = getVirtualScrollWindowPosition(top, virtualScrollWindowMetrics);
511
+ if (virtualScrollBaseRef.current !== position.base) {
512
+ virtualScrollBaseRef.current = position.base;
513
+ setVirtualScrollBase(position.base);
514
+ }
515
+ if (scrollContainer.scrollTop !== position.physicalScrollTop) {
516
+ scrollContainer.scrollTop = position.physicalScrollTop;
517
+ }
518
+ if (scrollContainer.scrollLeft !== left)
519
+ scrollContainer.scrollLeft = left;
520
+ latestScrollRef.current = { top: position.logicalScrollTop, left };
521
+ latestCommittedScrollRef.current = { top: position.logicalScrollTop, left };
522
+ setScroll(position.logicalScrollTop, left);
523
+ }, [setScroll, virtualScrollWindowMetrics]);
524
+ const handleScrollTopChange = useCallback((offset) => {
525
+ commitScroll(offset, scrollContainerRef.current?.scrollLeft ?? scrollLeft);
526
+ }, [commitScroll, scrollLeft]);
481
527
  const scheduleScrollFrame = useCallback(() => {
482
528
  if (scrollRafRef.current !== null)
483
529
  return;
@@ -486,90 +532,42 @@ function Table(props) {
486
532
  const scrollContainer = scrollContainerRef.current;
487
533
  if (!scrollContainer)
488
534
  return;
489
- const { scrollTop, scrollLeft } = scrollContainer;
490
- latestScrollRef.current = { top: scrollTop, left: scrollLeft };
535
+ const { scrollTop: physicalScrollTop, scrollLeft } = scrollContainer;
536
+ const position = rebaseVirtualScrollWindow(virtualScrollBaseRef.current, physicalScrollTop, virtualScrollWindowMetrics);
537
+ if (virtualScrollBaseRef.current !== position.base) {
538
+ virtualScrollBaseRef.current = position.base;
539
+ setVirtualScrollBase(position.base);
540
+ }
541
+ if (physicalScrollTop !== position.physicalScrollTop) {
542
+ scrollContainer.scrollTop = position.physicalScrollTop;
543
+ }
544
+ const logicalScrollTop = position.logicalScrollTop;
545
+ latestScrollRef.current = { top: logicalScrollTop, left: scrollLeft };
491
546
  const { top: committedTop, left: committedLeft } = latestCommittedScrollRef.current;
492
- if (committedTop !== scrollTop || committedLeft !== scrollLeft) {
493
- latestCommittedScrollRef.current = { top: scrollTop, left: scrollLeft };
494
- setScroll(scrollTop, scrollLeft);
547
+ if (committedTop !== logicalScrollTop || committedLeft !== scrollLeft) {
548
+ latestCommittedScrollRef.current = { top: logicalScrollTop, left: scrollLeft };
549
+ setScroll(logicalScrollTop, scrollLeft);
495
550
  }
496
551
  if (containerRef.current?.hasAttribute('data-bgrid-scrolling')) {
497
552
  scrollRafRef.current = requestAnimationFrame(pollNativeScrollPosition);
498
553
  }
499
554
  };
500
555
  scrollRafRef.current = requestAnimationFrame(pollNativeScrollPosition);
501
- }, [setScroll]);
556
+ }, [setScroll, virtualScrollWindowMetrics]);
502
557
  const onScroll = useCallback(() => {
503
558
  if (scrollContainerRef.current) {
504
- const { scrollTop, scrollLeft } = scrollContainerRef.current;
505
- latestScrollRef.current = { top: scrollTop, left: scrollLeft };
506
559
  markScrollActive();
507
560
  scheduleScrollFrame();
508
561
  }
509
562
  }, [markScrollActive, scheduleScrollFrame]);
510
- const onWheel = useCallback(evt => {
511
- const scrollContainer = scrollContainerRef.current;
512
- if (!scrollContainer)
513
- return;
514
- const delta = { x: 0, y: 0 };
515
- if (evt.detail) {
516
- delta.y = evt.detail * 10;
517
- }
518
- else if (typeof evt.deltaY === 'undefined') {
519
- delta.y = -evt.wheelDelta;
520
- delta.x = 0;
521
- }
522
- else {
523
- delta.y = evt.deltaY;
524
- delta.x = evt.deltaX;
525
- }
526
- if (evt.deltaMode === 1) {
527
- delta.y = delta.y * 16;
528
- delta.x = delta.x * 16;
529
- }
530
- else if (evt.deltaMode === 2) {
531
- delta.y = delta.y * scrollContainer.clientHeight;
532
- delta.x = delta.x * scrollContainer.clientWidth;
533
- }
534
- const maxScrollTop = Math.max(scrollContainer.scrollHeight - scrollContainer.clientHeight, 0);
535
- const maxScrollLeft = Math.max(scrollContainer.scrollWidth - scrollContainer.clientWidth, 0);
536
- const nextScrollTop = Math.min(maxScrollTop, Math.max(0, scrollContainer.scrollTop + delta.y));
537
- const nextScrollLeft = Math.min(maxScrollLeft, Math.max(0, scrollContainer.scrollLeft + delta.x));
538
- const didScrollTop = nextScrollTop !== scrollContainer.scrollTop;
539
- const didScrollLeft = nextScrollLeft !== scrollContainer.scrollLeft;
540
- const isVertical = Math.abs(delta.y) > Math.abs(delta.x);
541
- if (isVertical) {
542
- if (!didScrollTop)
543
- return;
544
- }
545
- else {
546
- if (!didScrollLeft)
547
- return;
548
- }
549
- evt.preventDefault();
550
- if (didScrollTop)
551
- scrollContainer.scrollTop = nextScrollTop;
552
- if (didScrollLeft)
553
- scrollContainer.scrollLeft = nextScrollLeft;
554
- }, []);
555
563
  const resetScrollPosition = useCallback((type) => {
556
- if (type === 'all' || type === 'left')
557
- scrollContainerRef.current?.scrollTo({ left: 0 });
558
- if (type === 'all' || type === 'top')
559
- scrollContainerRef.current?.scrollTo({ top: 0 });
560
- }, []);
561
- const commitScroll = useCallback((top, left) => {
562
564
  const scrollContainer = scrollContainerRef.current;
563
565
  if (!scrollContainer)
564
566
  return;
565
- if (scrollContainer.scrollTop !== top)
566
- scrollContainer.scrollTop = top;
567
- if (scrollContainer.scrollLeft !== left)
568
- scrollContainer.scrollLeft = left;
569
- latestScrollRef.current = { top, left };
570
- latestCommittedScrollRef.current = { top, left };
571
- setScroll(top, left);
572
- }, [setScroll]);
567
+ const nextTop = type === 'all' || type === 'top' ? 0 : latestScrollRef.current.top;
568
+ const nextLeft = type === 'all' || type === 'left' ? 0 : scrollContainer.scrollLeft;
569
+ commitScroll(nextTop, nextLeft);
570
+ }, [commitScroll]);
573
571
  useEffect(() => {
574
572
  if (!searchOpen || activeSearchMatchIndex === undefined)
575
573
  return;
@@ -593,6 +591,7 @@ function Table(props) {
593
591
  frozenRowCount,
594
592
  columns,
595
593
  rowHeight: trHeight,
594
+ verticalScrollState: logicalVerticalScrollState,
596
595
  viewportInsets,
597
596
  });
598
597
  if (result.didScroll)
@@ -602,6 +601,7 @@ function Table(props) {
602
601
  columns,
603
602
  commitScroll,
604
603
  frozenRowCount,
604
+ logicalVerticalScrollState,
605
605
  props.frozenColumnIndex,
606
606
  searchMatches,
607
607
  searchOpen,
@@ -833,18 +833,18 @@ function Table(props) {
833
833
  if (!dragState || !pointer || !scrollContainer || !bodyContainer)
834
834
  return;
835
835
  const delta = getSelectionAutoScrollDelta(pointer.clientX, pointer.clientY, bodyContainer, scrollContainer);
836
- const maxScrollTop = Math.max(scrollContainer.scrollHeight - scrollContainer.clientHeight, 0);
836
+ const maxScrollTop = virtualScrollWindowMetrics.logicalMaxScroll;
837
837
  const maxScrollLeft = Math.max(scrollContainer.scrollWidth - scrollContainer.clientWidth, 0);
838
- const nextScrollTop = clamp(scrollContainer.scrollTop + delta.y, 0, maxScrollTop);
838
+ const nextScrollTop = clamp(latestScrollRef.current.top + delta.y, 0, maxScrollTop);
839
839
  const nextScrollLeft = clamp(scrollContainer.scrollLeft + delta.x, 0, maxScrollLeft);
840
- if (nextScrollTop !== scrollContainer.scrollTop || nextScrollLeft !== scrollContainer.scrollLeft) {
840
+ if (nextScrollTop !== latestScrollRef.current.top || nextScrollLeft !== scrollContainer.scrollLeft) {
841
841
  commitScroll(nextScrollTop, nextScrollLeft);
842
842
  }
843
843
  updateSelectionByPointer(pointer.clientX, pointer.clientY);
844
844
  if (cellSelectionDragRef.current && (delta.x !== 0 || delta.y !== 0)) {
845
845
  selectionAutoScrollRafRef.current = requestAnimationFrame(runSelectionAutoScroll);
846
846
  }
847
- }, [commitScroll, updateSelectionByPointer]);
847
+ }, [commitScroll, updateSelectionByPointer, virtualScrollWindowMetrics.logicalMaxScroll]);
848
848
  const selectAllCells = useCallback(() => {
849
849
  if (!cellSelectionEnabled)
850
850
  return false;
@@ -1065,7 +1065,9 @@ function Table(props) {
1065
1065
  clipboardRow.forEach((clipboardValue, columnOffset) => {
1066
1066
  const columnIndex = startColumnIndex + columnOffset;
1067
1067
  const column = columns[columnIndex];
1068
- if (!column || column.editable === false)
1068
+ // Pasting mutates row data without opening an editor, so only columns
1069
+ // explicitly opted into editing may receive clipboard values.
1070
+ if (!column || column.editable !== true)
1069
1071
  return;
1070
1072
  const logicalCell = resolveLogicalCell(logicalResolutionData, props.cellMergeOptions, {
1071
1073
  rowIndex,
@@ -1497,8 +1499,6 @@ function Table(props) {
1497
1499
  if (scrollContainerRefCurrent) {
1498
1500
  scrollContainerRefCurrent.removeEventListener('scroll', onScroll);
1499
1501
  scrollContainerRefCurrent.addEventListener('scroll', onScroll, { passive: true, capture: true });
1500
- // scrollContainerRefCurrent.removeEventListener('wheel', onWheel);
1501
- // scrollContainerRefCurrent.addEventListener('wheel', onWheel, { passive: false, capture: true });
1502
1502
  }
1503
1503
  return () => {
1504
1504
  if (scrollRafRef.current !== null) {
@@ -1512,18 +1512,16 @@ function Table(props) {
1512
1512
  containerRefCurrent?.removeAttribute('data-bgrid-scrolling');
1513
1513
  scrollContainerRefCurrent?.removeEventListener('scroll', onScroll);
1514
1514
  };
1515
- }, [onScroll, onWheel, setInitialized]);
1515
+ }, [onScroll, setInitialized]);
1516
1516
  React.useLayoutEffect(() => {
1517
1517
  if (!scrollContainerRef.current)
1518
1518
  return;
1519
- if (props.scrollLeft !== undefined) {
1520
- scrollContainerRef.current.scrollLeft = props.scrollLeft;
1521
- }
1522
- if (props.scrollTop !== undefined) {
1523
- scrollContainerRef.current.scrollTop = props.scrollTop;
1524
- }
1525
- }, [props.scrollLeft, props.scrollTop]);
1519
+ const nextTop = props.scrollTop ?? latestScrollRef.current.top;
1520
+ const nextLeft = props.scrollLeft ?? latestScrollRef.current.left;
1521
+ commitScroll(nextTop, nextLeft);
1522
+ }, [commitScroll, props.scrollLeft, props.scrollTop]);
1526
1523
  useEffect(() => {
1524
+ latestScrollRef.current = { top: scrollTop, left: scrollLeft };
1527
1525
  latestCommittedScrollRef.current = { top: scrollTop, left: scrollLeft };
1528
1526
  }, [scrollTop, scrollLeft]);
1529
1527
  useEffect(() => {
@@ -1604,6 +1602,7 @@ function Table(props) {
1604
1602
  searchOpen,
1605
1603
  searchOptions,
1606
1604
  trHeight,
1605
+ verticalScrollState: logicalVerticalScrollState,
1607
1606
  viewportInsets: keyboardViewportInsets,
1608
1607
  });
1609
1608
  keyboardRuntimeRef.current = {
@@ -1636,6 +1635,7 @@ function Table(props) {
1636
1635
  searchOpen,
1637
1636
  searchOptions,
1638
1637
  trHeight,
1638
+ verticalScrollState: logicalVerticalScrollState,
1639
1639
  viewportInsets: keyboardViewportInsets,
1640
1640
  };
1641
1641
  useEffect(() => {
@@ -1651,7 +1651,7 @@ function Table(props) {
1651
1651
  }
1652
1652
  };
1653
1653
  const moveWithArrowKey = (key, extendSelection, toBoundary = false) => {
1654
- const { activeCell, cellInteractionSession, cellNavigationOptions, cellSelectionEnabled, columns, commitScroll, dataLength, frozenColumnIndex, frozenRowCount, moveActiveCell, trHeight, viewportInsets, } = keyboardRuntimeRef.current;
1654
+ const { activeCell, cellInteractionSession, cellNavigationOptions, cellSelectionEnabled, columns, commitScroll, dataLength, frozenColumnIndex, frozenRowCount, moveActiveCell, trHeight, verticalScrollState, viewportInsets, } = keyboardRuntimeRef.current;
1655
1655
  const container = containerRef.current;
1656
1656
  const activeElement = document.activeElement;
1657
1657
  if (!container ||
@@ -1685,6 +1685,7 @@ function Table(props) {
1685
1685
  frozenRowCount,
1686
1686
  columns,
1687
1687
  rowHeight: trHeight,
1688
+ verticalScrollState,
1688
1689
  viewportInsets,
1689
1690
  });
1690
1691
  if (result.didScroll) {
@@ -1716,7 +1717,7 @@ function Table(props) {
1716
1717
  arrowRepeatFrame = requestAnimationFrame(runArrowRepeatFrame);
1717
1718
  };
1718
1719
  const handleKeyDown = (evt) => {
1719
- const { activeCell, cellInteractionSession, cellNavigationOptions, cellSelectionEnabled, clearCellSelection, clearCellSelectionOnEscape, columns, commitCheckboxCell, commitScroll, copySelectedCells, data, dataLength, editable, editItemIndex, endCellSelectionDrag, frozenColumnIndex, frozenRowCount, height, handleClick, moveActiveCell, openCellContextMenu, selectAllCells, setEditItem, requestSearchOpen, searchOpen, searchOptions, trHeight, viewportInsets, } = keyboardRuntimeRef.current;
1720
+ const { activeCell, cellInteractionSession, cellNavigationOptions, cellSelectionEnabled, clearCellSelection, clearCellSelectionOnEscape, columns, commitCheckboxCell, commitScroll, copySelectedCells, data, dataLength, editable, editItemIndex, endCellSelectionDrag, frozenColumnIndex, frozenRowCount, height, handleClick, moveActiveCell, openCellContextMenu, selectAllCells, setEditItem, requestSearchOpen, searchOpen, searchOptions, trHeight, verticalScrollState, viewportInsets, } = keyboardRuntimeRef.current;
1720
1721
  const container = containerRef.current;
1721
1722
  const activeElement = document.activeElement;
1722
1723
  if (!container || (activeElement !== container && !container.contains(activeElement)))
@@ -1844,6 +1845,7 @@ function Table(props) {
1844
1845
  frozenRowCount,
1845
1846
  columns,
1846
1847
  rowHeight: trHeight,
1848
+ verticalScrollState,
1847
1849
  viewportInsets,
1848
1850
  });
1849
1851
  if (result.didScroll) {
@@ -1887,6 +1889,7 @@ function Table(props) {
1887
1889
  frozenRowCount,
1888
1890
  columns,
1889
1891
  rowHeight: trHeight,
1892
+ verticalScrollState,
1890
1893
  viewportInsets,
1891
1894
  });
1892
1895
  if (result.didScroll) {
@@ -1914,6 +1917,7 @@ function Table(props) {
1914
1917
  frozenRowCount,
1915
1918
  columns,
1916
1919
  rowHeight: trHeight,
1920
+ verticalScrollState,
1917
1921
  viewportInsets,
1918
1922
  });
1919
1923
  if (result.didScroll) {
@@ -2020,7 +2024,7 @@ function Table(props) {
2020
2024
  event.stopPropagation();
2021
2025
  }
2022
2026
  } },
2023
- React.createElement(ScrollPlane, { style: { height: scrollPlaneHeight, minWidth: scrollPlaneMinWidth } },
2027
+ React.createElement(ScrollPlane, { style: { height: scrollPlaneHeight, minWidth: scrollPlaneMinWidth }, "data-bgrid-virtual-scroll-window": virtualScrollWindowMetrics.enabled ? 'true' : undefined, "data-bgrid-logical-height": virtualScrollWindowMetrics.logicalContentHeight, "data-bgrid-physical-height": virtualScrollWindowMetrics.physicalContentHeight, "data-bgrid-logical-scroll-top": Math.round(scrollTop), "data-bgrid-virtual-scroll-base": virtualScrollWindowMetrics.enabled ? Math.round(virtualScrollBase) : undefined },
2024
2028
  React.createElement(HeaderContainer, { style: { height: headerHeight, top: 0 }, role: 'rfdg-header-container' },
2025
2029
  (frozenColumnsWidth ?? 0) > 0 && (React.createElement(FrozenHeader, { style: { width: frozenColumnsWidth }, role: 'rfdg-frozen-header' },
2026
2030
  React.createElement(TableHeadFrozen, { container: containerRef }))),
@@ -2031,7 +2035,7 @@ function Table(props) {
2031
2035
  React.createElement(TableSummaryFrozen, { position: 'top' }))),
2032
2036
  React.createElement(Summary, { style: { left: frozenColumnsWidth }, role: 'rfdg-summary' },
2033
2037
  React.createElement(TableSummary, { position: 'top' })))),
2034
- React.createElement("div", { className: 'bgrid-body-scroll-content', style: { height: frozenRowsHeight + visibleScrollableRows.scrollContentHeight } },
2038
+ React.createElement("div", { className: 'bgrid-body-scroll-content', style: { height: virtualScrollWindowMetrics.physicalContentHeight } },
2035
2039
  frozenRowCount > 0 && (React.createElement("div", { className: 'bgrid-frozen-rows-layer', style: { height: frozenRowsHeight, top: stickyTopHeight }, "data-bgrid-row-band": 'frozen' },
2036
2040
  (frozenColumnsWidth ?? 0) > 0 && (React.createElement("div", { className: 'bgrid-frozen-rows-left bgrid-frozen-column-boundary', style: { width: frozenColumnsWidth }, role: 'rfdg-frozen-rows-left' },
2037
2041
  React.createElement(TableBodyFrozen, { scrollContainerRef: scrollContainerRef, rowRange: frozenRowRange, role: 'rfdg-body-top-frozen', quadrant: 'top-left', allowRowReorder: false }),
@@ -2039,17 +2043,19 @@ function Table(props) {
2039
2043
  React.createElement("div", { className: 'bgrid-frozen-rows-main', style: { left: frozenColumnsWidth }, role: 'rfdg-frozen-rows-main' },
2040
2044
  React.createElement(TableBody, { scrollContainerRef: scrollContainerRef, rowRange: frozenRowRange, role: 'rfdg-body-top', quadrant: 'top-main', allowRowReorder: false }),
2041
2045
  renderSelectionOverlay('top-main')))),
2042
- React.createElement("div", { className: 'bgrid-scrollable-rows-layer', style: { height: visibleScrollableRows.scrollContentHeight }, "data-bgrid-row-band": 'scrollable' },
2046
+ React.createElement("div", { className: 'bgrid-scrollable-rows-layer', style: { height: physicalScrollableRowsHeight }, "data-bgrid-row-band": 'scrollable' },
2043
2047
  (frozenColumnsWidth ?? 0) > 0 && (React.createElement(FrozenScrollContent, { style: {
2044
2048
  width: frozenColumnsWidth,
2045
- height: visibleScrollableRows.scrollContentHeight,
2049
+ height: physicalScrollableRowsHeight,
2046
2050
  }, role: 'rfdg-frozen-scroll-container' },
2047
2051
  React.createElement(TableBodyFrozen, { scrollContainerRef: scrollContainerRef, rowRange: scrollableRowRange, style: frozenScrollableBodyStyle, quadrant: 'body-left', onRowReorderPointerDown: rowReorderController.onPointerDown, onRowReorderKeyDown: rowReorderController.onKeyDown }),
2048
2052
  renderSelectionOverlay('body-left'))),
2049
2053
  React.createElement(ScrollContent, { style: {
2050
2054
  left: frozenColumnsWidth,
2051
- paddingTop: visibleScrollableRows.paddingTop,
2052
- height: visibleScrollableRows.scrollContentHeight,
2055
+ paddingTop: virtualScrollWindowMetrics.enabled
2056
+ ? visibleScrollableRows.paddingTop - virtualScrollBase
2057
+ : visibleScrollableRows.paddingTop,
2058
+ height: physicalScrollableRowsHeight,
2053
2059
  } },
2054
2060
  React.createElement(TableBody, { scrollContainerRef: scrollContainerRef, rowRange: scrollableRowRange, quadrant: 'body-main' }),
2055
2061
  renderSelectionOverlay('body-main')))),
@@ -2139,7 +2145,7 @@ function getCellPositionFromPointer({ clientX, clientY, bodyContainer, scrollCon
2139
2145
  const rowIndex = metrics.frozenRowCount > 0 && clientY < frozenRowsBottom
2140
2146
  ? clamp(Math.floor((clientY - bodyRect.top) / metrics.trHeight), 0, metrics.frozenRowCount - 1)
2141
2147
  : clamp(metrics.frozenRowCount +
2142
- Math.floor((scrollContainer.scrollTop +
2148
+ Math.floor((metrics.scrollTop +
2143
2149
  clamp(clientY - scrollRect.top, 0, Math.max(scrollContainer.clientHeight - 1, 0))) /
2144
2150
  metrics.trHeight), 0, metrics.dataLength - 1);
2145
2151
  const columnIndex = getColumnIndexFromPointer({
@@ -1,14 +1,14 @@
1
1
  import * as React from 'react';
2
2
  import { clipCellSelectionFragment, } from '../../utils/cellSelectionGeometry';
3
- export function CellSelectionOverlayLayer({ quadrant, selectionFragments, activeFragments, activeFill, activeRing, viewport, }) {
3
+ export function CellSelectionOverlayLayer({ quadrant, selectionFragments, activeFragments, activeFill, activeRing, viewport, offsetTop = 0, }) {
4
4
  const quadrantSelectionFragments = getVisibleFragments(selectionFragments, quadrant, viewport);
5
5
  const quadrantActiveFragments = getVisibleFragments(activeFragments, quadrant, viewport);
6
6
  if (quadrantSelectionFragments.length === 0 && quadrantActiveFragments.length === 0)
7
7
  return null;
8
8
  return (React.createElement("div", { className: 'bgrid-cell-selection-overlay-layer', "data-bgrid-selection-quadrant": quadrant, "aria-hidden": 'true' },
9
- quadrantSelectionFragments.map(fragment => (React.createElement("div", { key: `selection-${fragment.rangeIndex}`, className: 'bgrid-cell-selection-fragment', "data-bgrid-selection-fragment": 'true', "data-bgrid-selection-range-index": fragment.rangeIndex, "data-edge-top": fragment.edges.top ? 'true' : undefined, "data-edge-right": fragment.edges.right ? 'true' : undefined, "data-edge-bottom": fragment.edges.bottom ? 'true' : undefined, "data-edge-left": fragment.edges.left ? 'true' : undefined, style: getFragmentStyle(fragment) }))),
9
+ quadrantSelectionFragments.map(fragment => (React.createElement("div", { key: `selection-${fragment.rangeIndex}`, className: 'bgrid-cell-selection-fragment', "data-bgrid-selection-fragment": 'true', "data-bgrid-selection-range-index": fragment.rangeIndex, "data-edge-top": fragment.edges.top ? 'true' : undefined, "data-edge-right": fragment.edges.right ? 'true' : undefined, "data-edge-bottom": fragment.edges.bottom ? 'true' : undefined, "data-edge-left": fragment.edges.left ? 'true' : undefined, style: getFragmentStyle(fragment, offsetTop) }))),
10
10
  (activeFill || activeRing) &&
11
- quadrantActiveFragments.map(fragment => (React.createElement("div", { key: `active-${fragment.rangeIndex}`, className: 'bgrid-cell-active-fragment', "data-bgrid-active-fragment": 'true', "data-active-fill": activeFill ? 'true' : undefined, "data-active-ring": activeRing ? 'true' : undefined, style: getFragmentStyle(fragment) })))));
11
+ quadrantActiveFragments.map(fragment => (React.createElement("div", { key: `active-${fragment.rangeIndex}`, className: 'bgrid-cell-active-fragment', "data-bgrid-active-fragment": 'true', "data-active-fill": activeFill ? 'true' : undefined, "data-active-ring": activeRing ? 'true' : undefined, style: getFragmentStyle(fragment, offsetTop) })))));
12
12
  }
13
13
  function getVisibleFragments(fragments, quadrant, viewport) {
14
14
  return fragments.flatMap(fragment => {
@@ -18,10 +18,10 @@ function getVisibleFragments(fragments, quadrant, viewport) {
18
18
  return visibleFragment ? [visibleFragment] : [];
19
19
  });
20
20
  }
21
- function getFragmentStyle(fragment) {
21
+ function getFragmentStyle(fragment, offsetTop) {
22
22
  return {
23
23
  left: fragment.left,
24
- top: fragment.top,
24
+ top: fragment.top - offsetTop,
25
25
  width: fragment.width,
26
26
  height: fragment.height,
27
27
  };
@@ -41,8 +41,8 @@ export function getRowBottom(rowIndex, rowHeight) {
41
41
  return 0;
42
42
  return (rowIndex + 1) * rowHeight;
43
43
  }
44
- export function ensureCellVisible({ cell, scrollContainer, frozenColumnCount = 0, frozenRowCount = 0, columns, rowHeight, viewportInsets, }) {
45
- const currentScrollTop = scrollContainer?.scrollTop ?? 0;
44
+ export function ensureCellVisible({ cell, scrollContainer, frozenColumnCount = 0, frozenRowCount = 0, columns, rowHeight, verticalScrollState, viewportInsets, }) {
45
+ const currentScrollTop = verticalScrollState?.scrollTop ?? scrollContainer?.scrollTop ?? 0;
46
46
  const currentScrollLeft = scrollContainer?.scrollLeft ?? 0;
47
47
  let nextScrollTop = currentScrollTop;
48
48
  let nextScrollLeft = currentScrollLeft;
@@ -52,7 +52,7 @@ export function ensureCellVisible({ cell, scrollContainer, frozenColumnCount = 0
52
52
  const rowTop = getRowTop(targetRowIndex, rowHeight);
53
53
  const rowBottom = getRowBottom(targetRowIndex, rowHeight);
54
54
  const clientHeight = scrollContainer.clientHeight;
55
- const scrollHeight = scrollContainer.scrollHeight;
55
+ const scrollHeight = verticalScrollState?.scrollHeight ?? scrollContainer.scrollHeight;
56
56
  const maxScrollTop = Math.max(0, scrollHeight - clientHeight);
57
57
  if (clientHeight > 0) {
58
58
  const topInset = clamp(viewportInsets?.top ?? 0, 0, clientHeight);
@@ -95,7 +95,7 @@ export function ensureCellVisible({ cell, scrollContainer, frozenColumnCount = 0
95
95
  const didScrollLeft = nextScrollLeft !== currentScrollLeft;
96
96
  const didScroll = didScrollTop || didScrollLeft;
97
97
  if (scrollContainer) {
98
- if (didScrollTop) {
98
+ if (didScrollTop && !verticalScrollState) {
99
99
  scrollContainer.scrollTop = nextScrollTop;
100
100
  }
101
101
  if (didScrollLeft) {
@@ -15,6 +15,7 @@ export * from './scrollbar';
15
15
  export * from './number';
16
16
  export * from './buildHeaderMatrix';
17
17
  export * from './getVisibleScrollableRowRange';
18
+ export * from './virtualScrollWindow';
18
19
  export * from './cellEditState';
19
20
  export * from './getCellSelectionAxisState';
20
21
  export * from './mergedCells';
@@ -0,0 +1,59 @@
1
+ export const BGRID_MAX_PHYSICAL_SCROLL_HEIGHT = 1000000;
2
+ export function getVirtualScrollWindowMetrics({ logicalContentHeight, viewportHeight, enabled, maxPhysicalHeight = BGRID_MAX_PHYSICAL_SCROLL_HEIGHT, }) {
3
+ const safeViewportHeight = Math.max(viewportHeight, 0);
4
+ const safeLogicalContentHeight = Math.max(logicalContentHeight, safeViewportHeight);
5
+ const safeMaxPhysicalHeight = Math.max(maxPhysicalHeight, safeViewportHeight);
6
+ const windowingEnabled = enabled && safeLogicalContentHeight > safeMaxPhysicalHeight;
7
+ const physicalContentHeight = windowingEnabled ? safeMaxPhysicalHeight : safeLogicalContentHeight;
8
+ return {
9
+ enabled: windowingEnabled,
10
+ logicalContentHeight: safeLogicalContentHeight,
11
+ physicalContentHeight,
12
+ logicalMaxScroll: Math.max(safeLogicalContentHeight - safeViewportHeight, 0),
13
+ physicalMaxScroll: Math.max(physicalContentHeight - safeViewportHeight, 0),
14
+ };
15
+ }
16
+ export function getLogicalScrollTop(base, physicalScrollTop, metrics) {
17
+ return clamp(base + physicalScrollTop, 0, metrics.logicalMaxScroll);
18
+ }
19
+ export function getVirtualScrollWindowPosition(logicalScrollTop, metrics) {
20
+ const logicalTop = clamp(logicalScrollTop, 0, metrics.logicalMaxScroll);
21
+ if (!metrics.enabled) {
22
+ return {
23
+ base: 0,
24
+ physicalScrollTop: logicalTop,
25
+ logicalScrollTop: logicalTop,
26
+ };
27
+ }
28
+ const preferredPhysicalTop = metrics.physicalMaxScroll / 2;
29
+ const maxBase = Math.max(metrics.logicalMaxScroll - metrics.physicalMaxScroll, 0);
30
+ const base = clamp(logicalTop - preferredPhysicalTop, 0, maxBase);
31
+ return {
32
+ base,
33
+ physicalScrollTop: logicalTop - base,
34
+ logicalScrollTop: logicalTop,
35
+ };
36
+ }
37
+ export function rebaseVirtualScrollWindow(base, physicalScrollTop, metrics) {
38
+ const logicalScrollTop = getLogicalScrollTop(base, physicalScrollTop, metrics);
39
+ if (!metrics.enabled || metrics.physicalMaxScroll <= 0) {
40
+ return getVirtualScrollWindowPosition(logicalScrollTop, metrics);
41
+ }
42
+ const lowerGuard = metrics.physicalMaxScroll * 0.25;
43
+ const upperGuard = metrics.physicalMaxScroll * 0.75;
44
+ const maxBase = Math.max(metrics.logicalMaxScroll - metrics.physicalMaxScroll, 0);
45
+ const canMoveBackward = base > 0;
46
+ const canMoveForward = base < maxBase;
47
+ if ((physicalScrollTop < lowerGuard && canMoveBackward) ||
48
+ (physicalScrollTop > upperGuard && canMoveForward)) {
49
+ return getVirtualScrollWindowPosition(logicalScrollTop, metrics);
50
+ }
51
+ return {
52
+ base,
53
+ physicalScrollTop: clamp(physicalScrollTop, 0, metrics.physicalMaxScroll),
54
+ logicalScrollTop,
55
+ };
56
+ }
57
+ function clamp(value, min, max) {
58
+ return Math.min(Math.max(value, min), max);
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beautiful-grid",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A beautiful, powerful, open-source React Data Grid.",
5
5
  "homepage": "https://bgrid.axisj.com",
6
6
  "bugs": {
@@ -12,6 +12,7 @@ interface Props {
12
12
  width: number;
13
13
  height: number;
14
14
  };
15
+ offsetTop?: number;
15
16
  }
16
17
  export declare function CellSelectionOverlay(props: Props): React.JSX.Element;
17
18
  export {};
@@ -13,6 +13,7 @@ interface Props {
13
13
  activeFill: boolean;
14
14
  activeRing: boolean;
15
15
  viewport: SelectionViewport;
16
+ offsetTop?: number;
16
17
  }
17
- export declare function CellSelectionOverlayLayer({ quadrant, selectionFragments, activeFragments, activeFill, activeRing, viewport, }: Props): React.JSX.Element | null;
18
+ export declare function CellSelectionOverlayLayer({ quadrant, selectionFragments, activeFragments, activeFill, activeRing, viewport, offsetTop, }: Props): React.JSX.Element | null;
18
19
  export {};
@@ -12,6 +12,10 @@ export interface EnsureCellVisibleParams {
12
12
  frozenRowCount?: number;
13
13
  columns: AppModelColumn<any>[];
14
14
  rowHeight: number;
15
+ verticalScrollState?: {
16
+ scrollTop: number;
17
+ scrollHeight: number;
18
+ };
15
19
  viewportInsets?: {
16
20
  top?: number;
17
21
  right?: number;
@@ -26,4 +30,4 @@ export interface EnsureCellVisibleResult {
26
30
  didScrollTop: boolean;
27
31
  didScrollLeft: boolean;
28
32
  }
29
- export declare function ensureCellVisible({ cell, scrollContainer, frozenColumnCount, frozenRowCount, columns, rowHeight, viewportInsets, }: EnsureCellVisibleParams): EnsureCellVisibleResult;
33
+ export declare function ensureCellVisible({ cell, scrollContainer, frozenColumnCount, frozenRowCount, columns, rowHeight, verticalScrollState, viewportInsets, }: EnsureCellVisibleParams): EnsureCellVisibleResult;
@@ -15,6 +15,7 @@ export * from './scrollbar';
15
15
  export * from './number';
16
16
  export * from './buildHeaderMatrix';
17
17
  export * from './getVisibleScrollableRowRange';
18
+ export * from './virtualScrollWindow';
18
19
  export * from './cellEditState';
19
20
  export * from './getCellSelectionAxisState';
20
21
  export * from './mergedCells';