beautiful-grid 1.0.2 → 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;
@@ -1499,8 +1499,6 @@ function Table(props) {
1499
1499
  if (scrollContainerRefCurrent) {
1500
1500
  scrollContainerRefCurrent.removeEventListener('scroll', onScroll);
1501
1501
  scrollContainerRefCurrent.addEventListener('scroll', onScroll, { passive: true, capture: true });
1502
- // scrollContainerRefCurrent.removeEventListener('wheel', onWheel);
1503
- // scrollContainerRefCurrent.addEventListener('wheel', onWheel, { passive: false, capture: true });
1504
1502
  }
1505
1503
  return () => {
1506
1504
  if (scrollRafRef.current !== null) {
@@ -1514,18 +1512,16 @@ function Table(props) {
1514
1512
  containerRefCurrent?.removeAttribute('data-bgrid-scrolling');
1515
1513
  scrollContainerRefCurrent?.removeEventListener('scroll', onScroll);
1516
1514
  };
1517
- }, [onScroll, onWheel, setInitialized]);
1515
+ }, [onScroll, setInitialized]);
1518
1516
  React.useLayoutEffect(() => {
1519
1517
  if (!scrollContainerRef.current)
1520
1518
  return;
1521
- if (props.scrollLeft !== undefined) {
1522
- scrollContainerRef.current.scrollLeft = props.scrollLeft;
1523
- }
1524
- if (props.scrollTop !== undefined) {
1525
- scrollContainerRef.current.scrollTop = props.scrollTop;
1526
- }
1527
- }, [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]);
1528
1523
  useEffect(() => {
1524
+ latestScrollRef.current = { top: scrollTop, left: scrollLeft };
1529
1525
  latestCommittedScrollRef.current = { top: scrollTop, left: scrollLeft };
1530
1526
  }, [scrollTop, scrollLeft]);
1531
1527
  useEffect(() => {
@@ -1606,6 +1602,7 @@ function Table(props) {
1606
1602
  searchOpen,
1607
1603
  searchOptions,
1608
1604
  trHeight,
1605
+ verticalScrollState: logicalVerticalScrollState,
1609
1606
  viewportInsets: keyboardViewportInsets,
1610
1607
  });
1611
1608
  keyboardRuntimeRef.current = {
@@ -1638,6 +1635,7 @@ function Table(props) {
1638
1635
  searchOpen,
1639
1636
  searchOptions,
1640
1637
  trHeight,
1638
+ verticalScrollState: logicalVerticalScrollState,
1641
1639
  viewportInsets: keyboardViewportInsets,
1642
1640
  };
1643
1641
  useEffect(() => {
@@ -1653,7 +1651,7 @@ function Table(props) {
1653
1651
  }
1654
1652
  };
1655
1653
  const moveWithArrowKey = (key, extendSelection, toBoundary = false) => {
1656
- 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;
1657
1655
  const container = containerRef.current;
1658
1656
  const activeElement = document.activeElement;
1659
1657
  if (!container ||
@@ -1687,6 +1685,7 @@ function Table(props) {
1687
1685
  frozenRowCount,
1688
1686
  columns,
1689
1687
  rowHeight: trHeight,
1688
+ verticalScrollState,
1690
1689
  viewportInsets,
1691
1690
  });
1692
1691
  if (result.didScroll) {
@@ -1718,7 +1717,7 @@ function Table(props) {
1718
1717
  arrowRepeatFrame = requestAnimationFrame(runArrowRepeatFrame);
1719
1718
  };
1720
1719
  const handleKeyDown = (evt) => {
1721
- 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;
1722
1721
  const container = containerRef.current;
1723
1722
  const activeElement = document.activeElement;
1724
1723
  if (!container || (activeElement !== container && !container.contains(activeElement)))
@@ -1846,6 +1845,7 @@ function Table(props) {
1846
1845
  frozenRowCount,
1847
1846
  columns,
1848
1847
  rowHeight: trHeight,
1848
+ verticalScrollState,
1849
1849
  viewportInsets,
1850
1850
  });
1851
1851
  if (result.didScroll) {
@@ -1889,6 +1889,7 @@ function Table(props) {
1889
1889
  frozenRowCount,
1890
1890
  columns,
1891
1891
  rowHeight: trHeight,
1892
+ verticalScrollState,
1892
1893
  viewportInsets,
1893
1894
  });
1894
1895
  if (result.didScroll) {
@@ -1916,6 +1917,7 @@ function Table(props) {
1916
1917
  frozenRowCount,
1917
1918
  columns,
1918
1919
  rowHeight: trHeight,
1920
+ verticalScrollState,
1919
1921
  viewportInsets,
1920
1922
  });
1921
1923
  if (result.didScroll) {
@@ -2022,7 +2024,7 @@ function Table(props) {
2022
2024
  event.stopPropagation();
2023
2025
  }
2024
2026
  } },
2025
- 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 },
2026
2028
  React.createElement(HeaderContainer, { style: { height: headerHeight, top: 0 }, role: 'rfdg-header-container' },
2027
2029
  (frozenColumnsWidth ?? 0) > 0 && (React.createElement(FrozenHeader, { style: { width: frozenColumnsWidth }, role: 'rfdg-frozen-header' },
2028
2030
  React.createElement(TableHeadFrozen, { container: containerRef }))),
@@ -2033,7 +2035,7 @@ function Table(props) {
2033
2035
  React.createElement(TableSummaryFrozen, { position: 'top' }))),
2034
2036
  React.createElement(Summary, { style: { left: frozenColumnsWidth }, role: 'rfdg-summary' },
2035
2037
  React.createElement(TableSummary, { position: 'top' })))),
2036
- 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 } },
2037
2039
  frozenRowCount > 0 && (React.createElement("div", { className: 'bgrid-frozen-rows-layer', style: { height: frozenRowsHeight, top: stickyTopHeight }, "data-bgrid-row-band": 'frozen' },
2038
2040
  (frozenColumnsWidth ?? 0) > 0 && (React.createElement("div", { className: 'bgrid-frozen-rows-left bgrid-frozen-column-boundary', style: { width: frozenColumnsWidth }, role: 'rfdg-frozen-rows-left' },
2039
2041
  React.createElement(TableBodyFrozen, { scrollContainerRef: scrollContainerRef, rowRange: frozenRowRange, role: 'rfdg-body-top-frozen', quadrant: 'top-left', allowRowReorder: false }),
@@ -2041,17 +2043,19 @@ function Table(props) {
2041
2043
  React.createElement("div", { className: 'bgrid-frozen-rows-main', style: { left: frozenColumnsWidth }, role: 'rfdg-frozen-rows-main' },
2042
2044
  React.createElement(TableBody, { scrollContainerRef: scrollContainerRef, rowRange: frozenRowRange, role: 'rfdg-body-top', quadrant: 'top-main', allowRowReorder: false }),
2043
2045
  renderSelectionOverlay('top-main')))),
2044
- 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' },
2045
2047
  (frozenColumnsWidth ?? 0) > 0 && (React.createElement(FrozenScrollContent, { style: {
2046
2048
  width: frozenColumnsWidth,
2047
- height: visibleScrollableRows.scrollContentHeight,
2049
+ height: physicalScrollableRowsHeight,
2048
2050
  }, role: 'rfdg-frozen-scroll-container' },
2049
2051
  React.createElement(TableBodyFrozen, { scrollContainerRef: scrollContainerRef, rowRange: scrollableRowRange, style: frozenScrollableBodyStyle, quadrant: 'body-left', onRowReorderPointerDown: rowReorderController.onPointerDown, onRowReorderKeyDown: rowReorderController.onKeyDown }),
2050
2052
  renderSelectionOverlay('body-left'))),
2051
2053
  React.createElement(ScrollContent, { style: {
2052
2054
  left: frozenColumnsWidth,
2053
- paddingTop: visibleScrollableRows.paddingTop,
2054
- height: visibleScrollableRows.scrollContentHeight,
2055
+ paddingTop: virtualScrollWindowMetrics.enabled
2056
+ ? visibleScrollableRows.paddingTop - virtualScrollBase
2057
+ : visibleScrollableRows.paddingTop,
2058
+ height: physicalScrollableRowsHeight,
2055
2059
  } },
2056
2060
  React.createElement(TableBody, { scrollContainerRef: scrollContainerRef, rowRange: scrollableRowRange, quadrant: 'body-main' }),
2057
2061
  renderSelectionOverlay('body-main')))),
@@ -2141,7 +2145,7 @@ function getCellPositionFromPointer({ clientX, clientY, bodyContainer, scrollCon
2141
2145
  const rowIndex = metrics.frozenRowCount > 0 && clientY < frozenRowsBottom
2142
2146
  ? clamp(Math.floor((clientY - bodyRect.top) / metrics.trHeight), 0, metrics.frozenRowCount - 1)
2143
2147
  : clamp(metrics.frozenRowCount +
2144
- Math.floor((scrollContainer.scrollTop +
2148
+ Math.floor((metrics.scrollTop +
2145
2149
  clamp(clientY - scrollRect.top, 0, Math.max(scrollContainer.clientHeight - 1, 0))) /
2146
2150
  metrics.trHeight), 0, metrics.dataLength - 1);
2147
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.2",
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';