@ehfuse/mui-virtual-data-table 1.0.2 → 1.0.4

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.
package/dist/index.js CHANGED
@@ -457,6 +457,7 @@ showScrollbar = true, }, ref) => {
457
457
  const [dragStart, setDragStart] = react.useState({ y: 0, scrollTop: 0 });
458
458
  const [thumbHeight, setThumbHeight] = react.useState(0);
459
459
  const [thumbTop, setThumbTop] = react.useState(0);
460
+ const [hasScrollableContent, setHasScrollableContent] = react.useState(false);
460
461
  // 드래그 스크롤 상태
461
462
  const [isDragScrolling, setIsDragScrolling] = react.useState(false);
462
463
  const [dragScrollStart, setDragScrollStart] = react.useState({
@@ -593,9 +594,33 @@ showScrollbar = true, }, ref) => {
593
594
  return containerRef.current;
594
595
  }
595
596
  // children 요소에서 스크롤 가능한 요소 찾기
597
+ // 중첩된 OverlayScrollbar의 영역은 제외 (다른 OverlayScrollbar의 container는 스킵)
596
598
  const childScrollableElements = containerRef.current.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
597
599
  for (const child of childScrollableElements) {
598
600
  const element = child;
601
+ // 이 요소가 다른 OverlayScrollbar의 container인지 확인
602
+ // (자신의 containerRef는 아니어야 하고, overlay-scrollbar-container 클래스를 가진 경우)
603
+ if (element !== containerRef.current &&
604
+ element.classList.contains("overlay-scrollbar-container")) {
605
+ // 중첩된 OverlayScrollbar의 container이므로 스킵
606
+ continue;
607
+ }
608
+ // 이 요소의 부모 중에 다른 OverlayScrollbar container가 있는지 확인
609
+ let parent = element.parentElement;
610
+ let isNestedInAnotherScrollbar = false;
611
+ while (parent && parent !== containerRef.current) {
612
+ if (parent.classList.contains("overlay-scrollbar-container") &&
613
+ parent !== containerRef.current) {
614
+ // 다른 OverlayScrollbar 내부의 요소이므로 스킵
615
+ isNestedInAnotherScrollbar = true;
616
+ break;
617
+ }
618
+ parent = parent.parentElement;
619
+ }
620
+ if (isNestedInAnotherScrollbar) {
621
+ continue;
622
+ }
623
+ // 스크롤 가능한 요소인지 확인
599
624
  if (element.scrollHeight > element.clientHeight + 2) {
600
625
  cachedScrollContainerRef.current = element;
601
626
  return element;
@@ -628,15 +653,18 @@ showScrollbar = true, }, ref) => {
628
653
  }, [clearHideTimer, finalAutoHideConfig.enabled]);
629
654
  // 스크롤바 위치 및 크기 업데이트
630
655
  const updateScrollbar = react.useCallback(() => {
631
- if (!scrollbarRef.current)
632
- return;
633
656
  const scrollableElement = findScrollableElement();
634
657
  if (!scrollableElement) {
635
658
  // 스크롤 불가능하면 숨김
636
659
  setScrollbarVisible(false);
660
+ setHasScrollableContent(false);
637
661
  clearHideTimer();
638
662
  return;
639
663
  }
664
+ // 스크롤 가능한 콘텐츠가 있음을 표시
665
+ setHasScrollableContent(true);
666
+ if (!scrollbarRef.current)
667
+ return;
640
668
  // 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
641
669
  if (!finalAutoHideConfig.enabled) {
642
670
  setScrollbarVisible(true);
@@ -894,10 +922,25 @@ showScrollbar = true, }, ref) => {
894
922
  const container = containerRef.current;
895
923
  if (container && !scrollableElement) {
896
924
  elementsToWatch.push(container);
897
- // children 요소들의 스크롤도 감지
925
+ // children 요소들의 스크롤도 감지 (중첩된 OverlayScrollbar 제외)
898
926
  const childScrollableElements = container.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
899
927
  childScrollableElements.forEach((child) => {
900
- elementsToWatch.push(child);
928
+ const element = child;
929
+ // 다른 OverlayScrollbar의 container는 제외
930
+ if (element !== container &&
931
+ element.classList.contains("overlay-scrollbar-container")) {
932
+ return;
933
+ }
934
+ // 부모 중에 다른 OverlayScrollbar container가 있으면 제외
935
+ let parent = element.parentElement;
936
+ while (parent && parent !== container) {
937
+ if (parent.classList.contains("overlay-scrollbar-container") &&
938
+ parent !== container) {
939
+ return; // 중첩된 OverlayScrollbar 내부이므로 제외
940
+ }
941
+ parent = parent.parentElement;
942
+ }
943
+ elementsToWatch.push(element);
901
944
  });
902
945
  }
903
946
  // 모든 요소에 이벤트 리스너 등록
@@ -1037,16 +1080,13 @@ showScrollbar = true, }, ref) => {
1037
1080
  }, [updateScrollbar]);
1038
1081
  // 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
1039
1082
  react.useLayoutEffect(() => {
1040
- const timer = setTimeout(() => {
1041
- setIsInitialized(true);
1042
- // 초기화 후 스크롤바 업데이트 (썸 높이 정확하게 계산)
1043
- updateScrollbar();
1044
- // 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
1045
- if (!finalAutoHideConfig.enabled && isScrollable()) {
1046
- setScrollbarVisible(true);
1047
- }
1048
- }, 0);
1049
- return () => clearTimeout(timer);
1083
+ setIsInitialized(true);
1084
+ // 초기화 직후 스크롤바 업데이트 (썸 높이 정확하게 계산)
1085
+ updateScrollbar();
1086
+ // 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
1087
+ if (!finalAutoHideConfig.enabled && isScrollable()) {
1088
+ setScrollbarVisible(true);
1089
+ }
1050
1090
  }, [isScrollable, updateScrollbar, finalAutoHideConfig.enabled]);
1051
1091
  // Resize observer로 크기 변경 감지
1052
1092
  react.useEffect(() => {
@@ -1148,7 +1188,7 @@ showScrollbar = true, }, ref) => {
1148
1188
  minHeight: 0, // flex shrink 허용
1149
1189
  display: "flex", // flex 컨테이너로 설정
1150
1190
  flexDirection: "column", // 세로 방향 정렬
1151
- }, children: children }) }), showScrollbar && (jsxRuntime.jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
1191
+ }, children: children }) }), showScrollbar && hasScrollableContent && (jsxRuntime.jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
1152
1192
  clearHideTimer();
1153
1193
  setScrollbarVisible(true);
1154
1194
  }, onMouseLeave: () => {
@@ -1211,7 +1251,7 @@ showScrollbar = true, }, ref) => {
1211
1251
  borderRadius: `${finalThumbConfig.radius}px`,
1212
1252
  cursor: "pointer",
1213
1253
  transition: "background-color 0.2s ease-in-out, opacity 0.2s ease-in-out",
1214
- } })] })), showScrollbar && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
1254
+ } })] })), showScrollbar && hasScrollableContent && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
1215
1255
  position: "absolute",
1216
1256
  top: `${finalTrackConfig.margin}px`,
1217
1257
  right: finalTrackConfig.alignment === "right"
@@ -1237,7 +1277,7 @@ showScrollbar = true, }, ref) => {
1237
1277
  : finalArrowsConfig.opacity
1238
1278
  : 0,
1239
1279
  transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
1240
- }, children: "\u25B2" })), showScrollbar && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
1280
+ }, children: "\u25B2" })), showScrollbar && hasScrollableContent && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
1241
1281
  position: "absolute",
1242
1282
  bottom: `${finalTrackConfig.margin}px`,
1243
1283
  right: finalTrackConfig.alignment === "right"
@@ -1365,6 +1405,7 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1365
1405
  const initialScrollTopRef = react.useRef(0);
1366
1406
  const totalDragDistanceRef = react.useRef(0);
1367
1407
  const isScrollDraggingRef = react.useRef(false); // OverlayScrollbar 드래그 스크롤 감지용
1408
+ const mouseDownPositionRef = react.useRef({ x: 0, y: 0 }); // 마우스 다운 시작 위치
1368
1409
  react.useRef(null);
1369
1410
  /**
1370
1411
  * 마우스 버튼 누름 이벤트 핸들러
@@ -1475,8 +1516,8 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1475
1516
  return;
1476
1517
  }
1477
1518
  window.lastRangeChangeTime = now;
1478
- // 더 보수적인 조건: 85% 지점에서 로드 (기존 VirtualDataTable 방식)
1479
- const bufferSize = Math.max(10, Math.floor(data.length * 0.15)); // 데이터의 15% 또는 최소 10개
1519
+ // 더 보수적인 조건: 90% 지점에서 로드 (기존 VirtualDataTable 방식)
1520
+ const bufferSize = Math.max(10, Math.floor(data.length * 0.1)); // 데이터의 10% 또는 최소 10개
1480
1521
  const shouldLoadMore = range.endIndex >= data.length - bufferSize;
1481
1522
  // 추가 조건: 최소 30개 이상의 데이터에서만 더 가져오기 실행
1482
1523
  const hasMinimumData = data.length >= 30;
@@ -1800,12 +1841,22 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1800
1841
  // react-virtuoso는 'data-index' 속성으로 index를 전달합니다
1801
1842
  const rowIndex = rest["data-index"] ?? 0;
1802
1843
  const isOddRow = rowIndex % 2 === 1;
1803
- return (jsxRuntime.jsx(material.TableRow, { ...rest, onMouseDown: () => {
1804
- // 마우스 다운 시 드래그 플래그 초기화
1844
+ return (jsxRuntime.jsx(material.TableRow, { ...rest, onMouseDown: (e) => {
1845
+ // 마우스 다운 시 드래그 플래그 초기화 및 시작 위치 저장
1805
1846
  isScrollDraggingRef.current = false;
1806
- }, onMouseMove: () => {
1807
- // 마우스가 눌린 상태로 움직이면 드래그로 간주
1808
- isScrollDraggingRef.current = true;
1847
+ mouseDownPositionRef.current = {
1848
+ x: e.clientX,
1849
+ y: e.clientY,
1850
+ };
1851
+ }, onMouseMove: (e) => {
1852
+ // 마우스가 5px 이상 움직였을 때만 드래그로 간주
1853
+ const deltaX = Math.abs(e.clientX - mouseDownPositionRef.current.x);
1854
+ const deltaY = Math.abs(e.clientY - mouseDownPositionRef.current.y);
1855
+ const dragThreshold = 5; // 5px 임계값
1856
+ if (deltaX > dragThreshold ||
1857
+ deltaY > dragThreshold) {
1858
+ isScrollDraggingRef.current = true;
1859
+ }
1809
1860
  }, onClick: () => {
1810
1861
  // 드래그 스크롤이 아니고, 아이템이 있고, onRowClick이 있을 때만 실행
1811
1862
  if (!isScrollDraggingRef.current &&